Blog ·
Planificar pools de IPv4 para un producto VPS
Las direcciones son el único insumo que no se compra un martes. Cómo dar forma a los pools, por qué arbitra la base de datos y qué pasa al agotarse uno.
Por Provibr
Todo lo demás en un producto VPS es elástico. Si hace falta más CPU, se compra un nodo. Si hace falta más disco, se compra disco. Si hacen falta más direcciones, toca una lista de espera, pagar a un intermediario o rellenar un formulario de justificación para un registro regional.
IPv4 es el único insumo que se comporta como un bien físico, y suele recibir la menor atención de diseño, porque al principio se tiene un /24 y 254 parece de sobra. Deja de parecer de sobra justo en el punto en el que rediseñarlo sale caro.
Empezar por lo que un bloque da de verdad
El número utilizable nunca es el tamaño del bloque. Dos direcciones se van a la red y a la difusión, y otra más a la puerta de enlace. En tamaños pequeños eso no es un error de redondeo: es la mayor parte del bloque.
| Bloque | Direcciones | Red + difusión | Puerta de enlace | Vendibles | Sobrecarga |
|---|---|---|---|---|---|
| /24 | 256 | 2 | 1 | 253 | 1% |
| /27 | 32 | 2 | 1 | 29 | 9% |
| /28 | 16 | 2 | 1 | 13 | 19% |
| /29 | 8 | 2 | 1 | 5 | 38% |
| /30 | 4 | 2 | 1 | 1 | 75% |
Un /29 suena a ocho direcciones justo hasta que se venden cinco. Quien compra espacio de direcciones en bloques pequeños debería presupuestar la sobrecarga como una partida más; en un /29 se tiran tres octavos de lo pagado.
Hay dos límites que merece la pena grabar en lo que se construya. Aceptamos como máximo un /8, porque para bloques más grandes una barra de uso deja de significar algo, y como mínimo un /30, porque /31 y /32 tienen cero direcciones utilizables con las reglas de host habituales y aceptarlos solo produce un pool que nunca puede asignar. Rechazarlos de forma explícita es mejor que aceptarlos y quedarse vacío.
Una sutileza más: se permite que la dirección de la puerta de enlace esté dentro del rango de asignación y simplemente se salta. Es deliberado, porque es la disposición más fácil de razonar para un cliente, pero significa que la capacidad de un pool es un cálculo y nunca un número almacenado. Cualquier cosa que lleve un contador de direcciones libres discrepará de la realidad el día que alguien mueva una puerta de enlace.
Dar forma a los pools según lo que se vende, no según lo que se compró
La unidad que ve un cliente es un plan. La unidad que se compró a un intermediario es un bloque. Casi nunca tienen la misma forma, y atarlas entre sí sale mal la primera vez que se compra un segundo bloque.
La disposición que ha aguantado: un plan apunta a un pool, y un pool es o bien un prefijo o bien un grupo de prefijos con nombre. Añadir capacidad a un producto es entonces añadir un prefijo a un grupo, que es una fila. El asignador recorre el grupo en orden y toma la primera dirección que consigue, así que un grupo es también un orden de consumo: el bloque que se quiera gastar primero va delante.
Subbloques, y el error que asigna dos veces en silencio
Los clientes no usan un /24 como una sola red plana. Lo trocean: un trozo detrás de un router, un trozo en otra VLAN, un trozo con su propia puerta de enlace. Así que un plan de direcciones necesita bloques hijos.
Aquí vive el fallo interesante, y merece la pena enunciarlo con precisión porque falla en silencio.
El mecanismo que impide que dos pedidos simultáneos obtengan la misma dirección es un índice único parcial sobre (prefijo, dirección) limitado a las filas que no se han liberado. Ese índice es por prefijo. Si dos filas de prefijo pueden repartir cada una la misma dirección, para el índice esas son dos direcciones distintas, y la plataforma entregará la misma IP a dos máquinas sin que nada, absolutamente nada, falle en el software.
El índice no puede arreglar eso. Tiene que arreglarlo la disposición. Así que el espacio de direcciones es una partición estricta, y las reglas que lo consiguen son:
- Un hijo cae por completo dentro de su padre y es estrictamente más pequeño.
- Los hermanos no pueden solaparse.
- El padre no reparte nada que caiga dentro de un bloque hijo. De la ventana del padre se recorta el bloque entero, y no solo el rango de asignación propio del hijo, porque eso haría que la respuesta dependiera de un rango que algún administrador mueve la semana que viene.
- El anidamiento tiene un solo nivel de profundidad.
Con esas cuatro, para una dirección como 192.0.2.70 la pregunta «¿a qué prefijo pertenece?» tiene siempre exactamente una respuesta, y el índice existente es suficiente.
Lo que hereda un hijo es otro conjunto de decisiones, y las respuestas no son uniformes. Puerta de enlace: nunca (un hijo sin puerta de enlace propia no tiene razón de existir). DNS: sí, si su propio campo está vacío, porque los resolutores pertenecen a la red y no al bloque. Rango de asignación: solo donde el rango del padre toca realmente al hijo, ya que el rango del padre se expresa en las direcciones del padre y normalmente no se solapa con el hijo en absoluto; heredarlo a ciegas produce una ventana vacía.
Dejar arbitrar a la base de datos
La tentación, cuando llegan dos pedidos a la vez, es bloquear algo o llevar un contador. Las dos son peores que la respuesta aburrida.
Los dos pedidos calculan la misma «dirección libre más baja» y los dos intentan insertar. Una inserción gana, la otra choca con la violación de unicidad, y el perdedor pasa al siguiente candidato. La base de datos es la única parte que ve a todos los escritores, incluido el que está en otro proceso, en otro trabajador o en otra pestaña del navegador.
El corolario importa a la hora de escribir el código: no comprobar primero. Leer «¿está libre esta dirección?» y después insertar abre justo la ventana que la restricción existe para cerrar. La inserción fallida es la respuesta a «¿estaba libre?», y es la única fiable.
Un detalle de implementación que pilla a mucha gente: muchos ORM envuelven los errores del controlador en su propio tipo de error, así que el SQLSTATE que interesa no está en la excepción que se captura sino en su causa. Conviene recorrer la cadena entera y usar el nombre de la restricción para decidir qué mensaje mostrar. Una sola tabla suele tener cuatro reglas de unicidad distintas, y hacer que todas digan lo mismo manda al cliente al campo equivocado.
Qué ocurre cuando un pool se seca
Detrás de «no hay dirección disponible» se esconden tres fallos distintos, y juntarlos produce un ticket de soporte cada vez:
| Situación | Qué significa | Qué debería leer el cliente |
|---|---|---|
| El plan no apunta a ningún pool | Configuración, no capacidad | El plan no tiene pool de direcciones |
| Un prefijo tiene datos inservibles | Una fila rota, las demás bien | Estos prefijos no se pudieron leer |
| Todos los candidatos están ocupados | Agotamiento real | Este pool está lleno, y aquí está cuál |
El del medio merece el código extra. Un solo prefijo con datos ilegibles no debería bloquear a los demás prefijos de su grupo: anotarlo, probar el resto y comunicarlo solo si no funcionó nada en absoluto. Si no, una fila mala tumba los pedidos de toda una línea de producto.
Y el mensaje de agotamiento debería nombrar el prefijo y su CIDR. «No hay IP disponible» manda a alguien a leer registros. «Pool lleno: customer-block-2 (198.51.100.0/28)» lo manda a comprar direcciones.
Devolver direcciones, y tener cuidado con el cuándo
La liberación es la mitad que va perdiendo capacidad sin hacer ruido.
Dos reglas resultaron importar más de lo esperado. La primera: liberar no es borrar la fila, sino poner una marca de tiempo de liberación. El índice único parcial solo mira las filas vivas, así que la dirección es reutilizable de inmediato mientras la pregunta sobre 192.0.2.10, «¿quién la tenía el año pasado?», sigue teniendo respuesta. Esa es casi toda la razón para gestionar direcciones, y es la pregunta que hace un informe de abuso.
La segunda: la liberación ocurre cuando la máquina está confirmada como desaparecida, no cuando se pidió el borrado. Solo lo que está al lado del hipervisor sabe que la VM desapareció de verdad; liberar al enviar el comando entrega la dirección al siguiente cliente mientras la máquina antigua sigue en marcha y sigue respondiendo en ella. El caso espejo es un pedido que falla a mitad: ahí la asignación se descarta en lugar de liberarse, porque nunca llegó a configurarse en nada.
Hay aquí una asimetría deliberada que conviene señalar, porque una capa más arriba tomamos la decisión contraria. Las direcciones se reutilizan, la libre más baja primero. Los identificadores de máquina no se reutilizan nunca. Un mensaje de estado tardío sobre una VM borrada que aterriza en una fila nueva con el mismo identificador corrompe el historial de dos máquinas; una dirección no lleva historial de esa forma, y no reutilizar ninguna agotaría para siempre un /24 después de 254 máquinas. Mismo producto, reglas opuestas, razones distintas.
Las reservas van en la misma tabla que las asignaciones
Habrá que apartar direcciones: un balanceador de carga, una puerta de enlace de un bloque que todavía no se ha desplegado, las cinco direcciones que alguien prometió a un cliente para el trimestre que viene.
Conviene ponerlas en la misma tabla que las asignaciones reales. Todo lector que pregunta «¿qué está ocupado?», el asignador, la barra de uso, la lista de sugerencias de un campo de dirección, las cuenta entonces automáticamente, y no hay una segunda administración que se descompase. Una tabla de reservas aparte es una segunda fuente de verdad que discrepará de la primera antes de un mes.
Con una salvedad que es fácil hacer mal: una dirección reservada no debe acabar nunca en la lista de permitidos del cortafuegos de una máquina. Esa lista se construye a partir de las asignaciones que pertenecen a un servidor, y una reserva no pertenece a ningún servidor. Si eso se hace mal, la VM de un cliente empieza a aceptar tráfico dirigido al balanceador de carga de otro.
Y el IPv6
Reconocemos la entrada de IPv6 y la rechazamos con educación en lugar de aceptarla y calcular algo verosímil: un camino de IPv6 a medias es más peligroso que ninguno, porque los números que produce parecen correctos. La decisión barata que se paga sola es guardar direcciones, puertas de enlace y servidores DNS como texto y no como un entero de 32 bits, de modo que añadir IPv6 más adelante sea código y no una migración de todas las filas propias.
La versión corta
- Presupuestar la sobrecarga. Un /29 son cinco direcciones vendibles, no ocho.
- Apuntar los planes a pools, y los pools a prefijos. Comprar más espacio debería ser una fila.
- Si se permiten subbloques, hacer del espacio de direcciones una partición estricta, o la regla de unicidad que protege será en silencio por prefijo y no protegerá nada.
- Dejar que la restricción de unicidad decida la carrera. Nunca comprobar primero.
- Liberar con la confirmación, no con la petición, y conservar el historial.
- Nombrar el pool en el mensaje de agotamiento. Es la diferencia entre un ticket y una orden de compra.
Esto salió de construir la gestión de direcciones en Provibr, donde las direcciones se emiten al hacer un pedido y se devuelven cuando el agente confirma que la máquina ha desaparecido de verdad.