Una VPN no es un producto. Son dos tareas atornilladas juntas, y tu máquina ya tiene un programa para cada una.

La primera tarea es fabricar un enlace virtual — una interfaz que parece una tarjeta de red, coge paquetes IP y los entrega en otro sitio. La segunda es transportar los bytes de ese enlace de un extremo al otro. Compra un aparato VPN y compras las dos tareas con una licencia grapada encima; hazlo tú y cada una es un programa ya instalado. Hay dos formas de hacer la primera tarea en Linux, y este artículo construye las dos: pppd, que trae todo un protocolo de enlace — direcciones negociadas, una comprobación de vida, tramado — y un dispositivo tap, que no trae nada y es un agujero en el núcleo por el que empujas tramas. Compromisos distintos, no competidores, y la segunda mitad los pone uno al lado del otro.

Ese protocolo es PPP, y la razón de que esto funcione está escrita en su primer párrafo: PPP es un protocolo de enlace para enlaces punto a punto1, y no especifica de qué está hecho el enlace. Un módem y una línea telefónica fueron la respuesta original. Nunca fueron la única. PPTP metió PPP dentro de GRE2. L2TP lo metió dentro de UDP3. PPPoE lo metió dentro de tramas Ethernet. Cada uno de ellos es el mismo protocolo con un mensajero distinto, y ninguno necesitó cambiar PPP para hacerlo.

Así que la pregunta no es si puedes ejecutar PPP sobre un socket TCP o UDP, sino cuál, y qué cuesta. La respuesta corta, con las cuentas a la vista: usa UDP. Ejecutar un protocolo de flujo dentro de un flujo fiable es el único error aquí que se ve bien en tu mesa y se desmonta en un enlace real.

Una VPN Son Dos Tareas. Ya Tienes Las Dos.

Quítale el marketing a cualquier túnel y pasan tres cosas: algo presenta una interfaz virtual y convierte paquetes en un flujo de bytes, algo transporta ese flujo por una red que ya funciona, y algo lo cifra, o nada lo hace.

Todo túnel es una mitad de enlace, un portador y algo de criptografíaLas mismas tres piezas siempre. Solo cambian el portador y la cripto.TÚNELMITAD DE ENLACEPORTADORCRIPTOPPP conmutado, 1994PPPmódem y línea telefónicaningunaPPPoEPPPtramas EthernetningunaPPTPPPPGREMPPE — roto, no lo hagasL2TP sobre IPsecPPPUDPIPsecEste artículo, construcción unoPPP sobre un ptysocket UDP, vía netcatDTLSEste artículo, construcción dosdispositivo tun o tapsocket UDP, vía socatDTLSWireGuardinterfaz del núcleoUDPNoise, y nada que negociar
Todos los túneles de esta lista tienen la misma forma. Las diferencias son en qué portador van los bytes, y si algo los cifra. PPP hace la mitad del enlace desde 1994, por eso aparece debajo de tres de ellos sin haber sido rediseñado para ninguno. Un dispositivo tun o tap hace esa misma mitad sin protocolo alguno, y esa es la segunda construcción de este artículo.

PPP hace la mitad del enlace como es debido — negociación de direcciones, una comprobación de vida, compresión de cabeceras, varios protocolos de capa de red sobre un enlace, un paso de autenticación si lo quieres — un montón de trabajo terminado en /usr/sbin sin hacer nada. Lo que no hace es preocuparse por el portador: dale un descriptor de fichero que mueva bytes en ambos sentidos y funciona encima de él. Netcat es un programa cuyo propósito entero es ser ese descriptor de fichero.

El cifrado es la parte que nadie te da hecha, y es la parte en la que se gasta la mayoría de este artículo, porque netcat no tiene respuesta para ella y fingir lo contrario es como la gente acaba construyendo cosas que no debería.

Las Fases, Y Por Qué Van En Este Orden

Todo lo de abajo se construye por fases, y cada una añade una cosa a la anterior. Eso no es un recurso didáctico. Es como deberías construirlo, porque cuando la fase seis se rompa necesitas saber si la fase dos sigue funcionando, y eso solo lo sabes si la fase dos fue alguna vez algo que ejecutaste por separado.

Seis fases, cada una añade una cosa que puedes probar por separadoConstrúyelo en este orden y siempre podrás volver a bajar1En crudo, sobre TCPpppd con un pty, o un dispositivo tap, y un servicio de escucha netcat a secas.Funciona en un banco. Se derrite en un camino real — dos temporizadores de retransmisión peleando.2En crudo, sobre UDPEl mismo enlace, portador de datagramas. Un datagrama perdido es un paquete perdido y nada más.Este es el cimiento. Todo lo de encima es opcional; esto no.3TLS, sobre TCPncat, stunnel u openssl envolviendo el portador. Verifica el certificado en los dos extremos,o tendrás cifrado sin identidad y ningún control de acceso.4DTLS, sobre UDPsocat u openssl, y la forma a la que apuntar: cifrada, autenticada, todavía datagramas.Un paquete perdido sigue siendo un paquete perdido en vez de dos pilas discutiendo.5Comprimidazstd entre la interfaz y el portador. Mantén la ventana entre tramas donde el portadorentregue en orden; una trama autónoma por datagrama, más un diccionario, donde no.6Las dos, en ese ordenComprimir y después cifrar. Nunca al revés — el texto cifrado no se comprime.Y conoce el precio: comprimir antes de cifrar filtra la longitud del texto plano. Bien con tráfico propio.
Seis fases, cada una añade una cosa a la de debajo. En crudo sobre TCP es donde empieza todo el mundo, y el único peldaño sin salida: funciona en un banco de pruebas y se derrite en un camino real. En crudo sobre UDP es el cimiento sobre el que se apoya todo lo demás. Después el cifrado, después la compresión, después las dos juntas. Cada fase es un enlace que puedes levantar, cruzar con un ping y dejar en marcha, así que cuando la parte alta de la escalera se porte mal puedes bajarla peldaño a peldaño.

El enlace mismo se construye igual — el razonamiento detrás de la rampa de un segundo está más abajo: un túnel se levanta en crudo, demuestra que puede llevar una trama, y solo entonces se pone listo. Una construcción que lo enciende todo a la vez falla como un bloque, y te pasas la tarde adivinando qué capa lo hizo.

Lo Que pppd Quiere En Realidad Es Un Descriptor De Fichero

pppd se escribió para puertos serie, así que la lectura ingenua es que necesita uno. No lo necesita. Necesita un dispositivo de terminal, y se lo fabrica él mismo:

pty script — Specifies that the command script is to be used to communicate rather than a specific terminal device. Pppd will allocate itself a pseudo-tty master/slave pair and use the slave as its terminal device. The script will be run in a child process with the pseudo-tty master as its standard input and output.4

Léelo otra vez — la construcción entera está ahí. pppd fabrica un pseudoterminal, se queda el esclavo y le entrega el maestro a un comando como stdin y stdout. Lo que ese comando haga con los bytes no es asunto de pppd. Ejecuta nc ahí y se van por un socket.

pppd, un pseudoterminal, netcat y un socketUn paquete, de ppp0 a ppp0, y todas las manos por las que pasaEQUIPO AEQUIPO Bppp0interfaz del núcleopppdtramado HDLCpar de ptyesclavo para pppdmaestro para el hijoncatstdin y stdoutpar de ptymaestro para el hijoesclavo para pppdncatstdin y stdoutpppdtramado HDLCppp0interfaz del núcleoel socket — TCP o UDPLas dos cajas del medio son la única parte que eliges. Todo lo de los lados es igual tanto si el portador es una línea telefónica,una consola serie, un flujo TCP, un flujo de datagramas UDP o una sesión TLS — por eso cambiar el portador después cuesta una palabra.Un pseudoterminal no tiene pin de detección de portadora, así que pppd espera una portadora que nunca llega. La opción `local` es lo que lo para.
pppd habla HDLC asíncrono hacia el lado esclavo de un pseudoterminal que se ha asignado él mismo. El comando que nombra pty hereda el lado maestro como stdin y stdout. Netcat copia stdin al socket y el socket a stdout, así que las dos instancias de pppd se hablan a través del par de pty y de la red sin que ninguna sepa que existe un socket.

Hay una segunda vía, notty, que usa el stdin y el stdout del propio pppd. Pero lanza un proceso de trasvase de caracteres por el que pasa cada byte, así que «increases the latency and CPU overhead»4. Usa pty salvo que tengas una razón para no hacerlo.

Tres hechos prácticos antes del primer comando, todos comprobados en la máquina en la que se escribió esto — Fedora 44, ppp-2.5.1-7.fc44:

$ pppd --version
pppd version 2.5.1

$ ls -l /usr/bin/pppd /dev/ppp
-rwxr-xr-x. 1 root root 393784 Jan 17  2026 /usr/bin/pppd
crw-------. 1 root root 108, 0 Sep 14 08:34 /dev/ppp

$ pppd noauth nodetach pty 'true'
pppd: using the noauth option requires root privilege

Necesita root. No hay forma de rodearlo. El binario no es setuid en Fedora, /dev/ppp está en modo 0600 y pertenece a root, y las opciones que necesitas son privilegiadas de todos modos. Esto es algo que ejecutas como root o bajo un fichero de unidad, no algo que un usuario hace a la ligera. Eso importa más adelante, cuando lleguemos a lo que significa para tu política de salida.

El lado del núcleo es modular. ppp_generic hace la interfaz, ppp_async el tramado con relleno de bytes que necesita un pty, y ppp_deflate/bsd_comp/ppp_mppe la compresión y el cifrado; se cargan bajo demanda. Si a una imagen de contenedor recortada le falta ppp_async, el enlace se levanta y no lleva nada — media hora miserable si no sabes dónde mirar.

Las líneas de control de módem no existen en un pty. Sin pin de detección de portadora, pppd espera una portadora que nunca llega. El arreglo es una palabra, local, que le dice que ignore las líneas de control de módem4. Déjala fuera y no pasa nada, sin un error que merezca la pena leer.

Netcat No Es Un Solo Programa

Antes de la construcción, la trampa que más tiempo come: «netcat» son al menos cuatro programas con banderas incompatibles, y cuál te toca depende de tu distribución. En esta máquina /usr/bin/nc es un enlace simbólico a /usr/bin/ncat, la reescritura de Nmap. Así que un comando copiado de una página wiki de hace quince años falla por razones que no tienen nada que ver con PPP.

ImplementaciónEscuchar en un puertoUDPSeguir en pie tras irse un par
Ncat (Nmap)ncat -l 443-u-k / --keep-open
OpenBSD netcatnc -l 443-u-k
Traditional netcatnc -l -p 443-uno
GNU netcatnc -l -p 443-uno

Así que comprueba cuál tienes antes de culpar a pppd:

readlink -f "$(command -v nc)"
nc --version 2>&1 | head -1 || nc -h 2>&1 | head -1

Abajo usaré ncat de forma explícita. Es el que trae TLS incorporado, cosa que importa más adelante, y ser explícito significa que los comandos no significan otra cosa en silencio en tu máquina.

Fase Uno: La Construcción TCP, Porque Es Lo Que Todo El Mundo Prueba Primero

Un extremo escucha, otro conecta. Las direcciones de aquí son del rango de documentación, así que pégalas tal cual en un laboratorio y no se daña nada enrutable. El puerto es 443 en todo el artículo, y es deliberado: el 443 saliente está abierto por defecto en casi cualquier red. Envuelve el portador en TLS un par de fases más abajo y el tráfico que lleva es indistinguible de cualquier sesión HTTPS. Atar un servicio de escucha ahí necesita root, y el extremo lejano — la máquina del propio atacante, o un relé — lo tiene. Ese es el argumento entero de la salida metido en un número de puerto, y volveré sobre él.

En el extremo que escucha:

pppd nodetach noauth local passive \
     nodefaultroute noipdefault \
     192.0.2.1:192.0.2.2 \
     lcp-echo-interval 10 lcp-echo-failure 3 \
     mtu 1400 mru 1400 \
     pty 'ncat --listen --keep-open 443'

En el extremo que conecta:

pppd nodetach noauth local \
     nodefaultroute noipdefault \
     192.0.2.2:192.0.2.1 \
     lcp-echo-interval 10 lcp-echo-failure 3 \
     mtu 1400 mru 1400 \
     pty 'ncat 198.51.100.10 443'

Cada palabra de ahí hace un trabajo, y una de ellas, noauth, hace un daño silencioso. El túnel no tiene control de acceso, y eso tiene su propia sección más adelante.

Cada palabra de la línea de pppd, y qué haceLa línea de comando de la construcción UDP, palabra a palabranodetachprimer plano, así ves su salida y puedes ponerlo bajo un supervisornoauthsin autenticación del par — este es el agujero; el túnel no tiene control de accesolocalignora las líneas de control de módem que un pty no tiene. Sin ella no se levanta nadapassiveespera un paquete LCP válido en vez de salir — comportamiento de socket servidornodefaultrouteno te quedes con la ruta por defecto al terminar IPCP. Pon tú la ruta que quierasnoipdefaultno ofrezcas la propia dirección de la máquina como extremo local192.0.2.1:192.0.2.2local:remoto, fijado — pppd rechaza cualquier otra respuesta durante IPCPlcp-echo-interval / -failurela comprobación de vida — su propia sección más abajomtu / mru 1400deja sitio para lo que el portador envuelva alrededor de cada tramapty '...'el comando cuyos stdin y stdout se convierten en el enlace — aquí, un socket netcatEl extremo que conecta es la misma línea sin `passive`: inicia en vez de esperar.
Cada opción de la línea, y qué hace. La que hay que vigilar es noauth: quita la autenticación del par, así que quien escucha acepta a quien llegue. nodefaultroute y noipdefault impiden que el enlace reescriba tu enrutado en silencio, local hace que llegue a levantarse sobre un pty, y el par local:remoto fijado impide que pppd acepte otra respuesta durante IPCP. El extremo que conecta es la misma línea sin passive.

Levanta los dos extremos y tienes un ppp0 en cada uno, una ruta punto a punto a la dirección lejana, y una interfaz que puedes hacer ping, enrutar y pasar por tcpdump. Eso es una VPN — sin instalar un paquete, sin configurar un demonio, sin intercambiar una clave, que es el problema, y volveré sobre él.

Qué Negoció, Y Cómo Mirarlo

Añade debug y pppd registra el intercambio del protocolo de control, que merece la pena leer una vez aunque no lo leas nunca más. El enlace se levanta por etapas, y cada etapa puede fallar por su cuenta.

LCP, luego la autenticación, luego un protocolo de control por familia de direccionesTres etapas, y cada una falla por su propia razón1LCP — el enlace en síUnidad máxima de recepción, el mapa de control,un número mágico para ver una línea en bucle, ysi algún extremo quiere que el otro se autentique.Falla aquí y el portador no está pasando los byteslimpiamente en ambos sentidos. Mira el socket,no las opciones de PPP.2Autenticación — opcionalPAP manda una contraseña en claro. CHAP haceun desafío y una respuesta MD5. Aquí se salta.Los dos demuestran quién es el par. Ninguno cifraun solo byte de lo que sigue.3aIPCPLa dirección IPv4de cada extremo.3bIPV6CPLos identificadores deinterfaz de 64 bits.Estos dos son independientes. IPv6 puede levantarse mientras IPv4 sigue discutiendo,y un fallo en uno no tira el otro. La interfaz empieza a llevar tráfico de una familiaen cuanto termina el protocolo de control de esa familia.
LCP arregla el enlace en sí — cómo de grande puede ser una trama, qué caracteres de control hay que escapar, y un número mágico que detecta una línea en bucle. La autenticación es opcional y aquí se salta. Después un protocolo de control por capa de red: IPCP para IPv4, IPV6CP para IPv6. Son independientes, así que un enlace puede llevar IPv6 mientras IPv4 sigue discutiendo, y un fallo en uno no tira el otro.

Las dos herramientas útiles de depuración ya están instaladas y nadie las usa:

# registra cada trama en ambos sentidos a un fichero
pppd ... debug record /tmp/ppp-trace

# después léelo en una forma legible
pppdump -h /tmp/ppp-trace | less

record escribe una captura con marcas de tiempo de cada byte, pppdump la convierte en algo legible4, y con tcpdump -ni ppp0 ves los dos lados — el tramado de debajo y los paquetes de encima.

La Trama En El Cable, Y Por Qué 0x7E Está En Todas Partes

PPP sobre una línea serie — y un pty lo es en lo que a pppd respecta — usa tramado HDLC asíncrono, y entenderlo es la diferencia entre ajustar esto y adivinar. Cada trama empieza y termina con el mismo byte: «Each frame begins and ends with a Flag Sequence, which is the binary sequence 01111110 (hexadecimal 0x7e)»5. Si 0x7e marca una frontera, no puede aparecer dentro de una trama, así que se escapa. También el byte de escape 0x7d, y cualquier otra cosa que pida uno de los dos extremos.

La trama, el byte de bandera, y lo que cuesta escaparUna trama, y los dos bytes que nunca pueden aparecer dentro0x7Ebandera0xFFdirección0x03controlprotocolo1 o 2 bytescarga — tu paquete IPhasta la unidad máxima de recepción acordadaFCSCRC de 16 bits0x7EbanderaEL ESCAPE, LA ÚNICA REGLA QUE HAYTodo byte que pudiera confundirse con una frontera se sustituye por 0x7D seguido del byte original en O exclusiva con 0x20.carga 0x7E transmitida como 0x7D 0x5Ecarga 0x7D transmitida como 0x7D 0x5DEsos dos son obligatorios. Todo lo demás lo decide el mapa de caracteres de control — 32 bits, uno por carácter de control.Mapa a cero — el valor por defecto de pppd, y correcto en un socketDos bytes escapados de 256. Sobrecarga que no medirás jamás. Un socket es un camino limpio de 8 bits y no necesita nada más.Los 32 marcados — el ajuste conservador de módemEn cargas comprimidas o cifradas alrededor de un byte de cada ocho está por debajo de 0x20, así que se duplica. Cerca del 12 % del enlace, para nada.
La trama es bandera, dirección, control, protocolo, carga, secuencia de verificación de trama, bandera. Cualquier byte dentro que pudiera confundirse con una frontera se sustituye por 0x7D seguido del byte original XOR 0x20. La ACCM decide cuántos otros bytes reciben el mismo trato: cero en un camino limpio de 8 bits, treinta y dos si uno de los extremos pide el valor conservador por defecto que supone un módem comiéndose los caracteres de control.

La regla de escape es exacta: «Each Flag Sequence, Control Escape octet, and any octet which is flagged in the sending Async-Control-Character-Map (ACCM), is replaced by a two octet sequence consisting of the Control Escape octet followed by the original octet exclusive-or’d with hexadecimal 0x20»5.

Esa última parte es el mando de ajuste. La ACCM son 32 bits, uno por carácter de control, y un 1 significa «escapa este». Pídelos todos y, en cargas cifradas donde los bytes son en la práctica aleatorios, más o menos uno de cada ocho bytes está por debajo de 0x20 — un 12 % de sobrecarga para nada.

El pppd moderno ya hace lo correcto aquí. Lee la página de manual en vez del folclore:

If no asyncmap option is given, the default is zero, so pppd will ask the peer not to escape any control characters.4

Así que asyncmap 0 es el valor por defecto, no un truco, y un socket es un camino limpio de 8 bits que no necesita escapes más allá de los dos bytes obligatorios. Déjalo en paz; pon bits solo cuando algo por el medio se coma de verdad los caracteres de control — un servidor de terminales, un concentrador serie, un proxy de consola malo. La secuencia de verificación de trama del final es un CRC de 16 bits, y es lo que hace que funcione la construcción UDP — que viene ahora.

TCP Sobre TCP Es El Portador Equivocado

La construcción de arriba funciona. En un banco de laboratorio, sobre loopback o una LAN tranquila, funciona de maravilla, que es justo por lo que la gente la pone en producción.

Después se encuentra con un camino real con pérdida real. Se cae de una forma que parece cualquier cosa menos lo que es.

El problema son dos temporizadores de retransmisión independientes apilados uno sobre otro, con el exterior escondiéndole la pérdida al interior. Olaf Titz escribió la explicación definitiva, y abre precisamente con esta construcción:

A frequently occurring idea for IP tunneling applications is to run a protocol like PPP, which encapsulates IP packets in a format suited for a stream transport (like a modem line), over a TCP-based connection. […] Unfortunately, it doesn’t work well. Long delays and frequent connection aborts are to be expected.6

Un paquete perdido, dos portadores, dos desenlaces muy distintosSe pierde un paquete. Lo que hace después cada portador.PORTADOR TCP — el túnel esconde la pérdida1El portador pierde un segmento.2El portador lo retransmite. Los bytes llegan tarde,no faltan — y ese es el problema entero.3El TCP tunelizado no ve el portador. Lee elretraso como congestión, dobla su temporizador yretransmite datos que ya vuelan por debajo.4Ahora el portador debe el original y un duplicado,por un enlace que acaba de probar que pierde paquetes.La cola crece más rápido de lo que las capas la vacían.El rendimiento se hunde mucho antes que el enlace.PORTADOR UDP — la pérdida llega a la capa a la que pertenece1El portador tira el paquete y no dice nada.2La trama PPP de dentro falla su suma de verificacióny se descarta. El siguiente byte de bandera resincroniza.3El TCP tunelizado ve una pérdida real, porque poruna vez le han dicho la verdad sobre el camino.4Reduce su ventana a la mitad y retransmite una vez.El control de congestión hace exactamente aquellopara lo que se diseñó. Un paquete perdido cuesta uno.
El TCP portador garantiza la entrega, así que un segmento perdido se retransmite y los bytes llegan tarde en vez de no llegar. El TCP tunelizado de dentro solo ve el retraso, decide que la red está congestionada, se echa atrás y retransmite los mismos datos — que el portador tiene ahora que entregar también. El temporizador de cada capa intenta arreglar un problema que la otra capa ya tiene suyo, y la cola crece más rápido de lo que ninguna puede vaciarla. Un portador UDP tira el paquete, el TCP interior ve una pérdida real, y su control de congestión hace el trabajo para el que fue diseñado.

Esta es la forma. Los dos TCP fijan un temporizador de retransmisión a partir de su tiempo de ida y vuelta. Cuando el portador pierde un segmento, retransmite, así que los datos de la conexión interior llegan tarde. Y el TCP interior, que no ve el portador, lee «tarde» como congestión y retransmite también. Ahora el portador tiene el original y un duplicado que entregar por un enlace que ya está perdiendo paquetes. El temporizador interior se dobla, la cola exterior crece, y el rendimiento se hunde mucho antes que el enlace. Nada en los registros dice por qué.

Esto no es un punto sutil de eficiencia. Es la diferencia entre un túnel que se degrada con elegancia y otro que deja de pasar tráfico con quizá un 2 % de pérdida mientras ping por ese mismo camino sigue viéndose bien — la razón para usar UDP, y para guardar TCP en la reserva solo para un camino que no vaya a pasar ninguna otra cosa.

Así que usa UDP — como valor por defecto, no como preferencia.

Fase Dos: La Construcción UDP, Que Es El Cimiento

El mismo pppd, otro portador. Lo único que cambia es el comando de pty.

Extremo que escucha:

pppd nodetach noauth local passive \
     nodefaultroute noipdefault \
     192.0.2.1:192.0.2.2 \
     lcp-echo-interval 10 lcp-echo-failure 3 \
     mtu 1400 mru 1400 \
     pty 'ncat --udp --listen 198.51.100.10 443'

Extremo que conecta:

pppd nodetach noauth local \
     nodefaultroute noipdefault \
     192.0.2.2:192.0.2.1 \
     lcp-echo-interval 10 lcp-echo-failure 3 \
     mtu 1400 mru 1400 \
     pty 'ncat --udp 198.51.100.10 443'

Dos cosas de un servicio de escucha UDP que te van a pillar, y ninguna es un problema de PPP.

Quien escucha no puede responder hasta que le hablan. Un socket UDP no tiene conexión que aceptar, así que netcat no puede saber a dónde mandar las respuestas hasta que llega un datagrama. Se engancha a la primera dirección y puerto de origen que oye y le habla a eso. Esto significa que el extremo que conecta tiene que hablar primero — cosa que pppd hace solo, porque el extremo no pasivo empieza a disparar peticiones de configuración LCP de inmediato. También significa que si el puerto de origen del cliente cambia, el portador está hablando en silencio con el sitio equivocado. Detrás de un NAT con un tiempo de espera UDP corto, eso es un túnel que se muere cada pocos minutos sin razón visible.

--keep-open no hace lo que quieres en UDP. El -k de Ncat mantiene un servicio de escucha TCP aceptando después de que un par se vaya. En UDP no hay nada que aceptar, así que recuperarse significa reiniciar el portador — para lo que están persist y holdoff, más abajo.

Las dos son argumentos a favor de socat, que trata a los pares UDP con más cuidado, y a favor de un supervisor en vez de confiar en que se quede en pie.

Por Qué PPP Sobrevive A Un Datagrama Perdido

La objeción evidente a un portador UDP es que PPP espera un flujo de bytes y UDP no lo es: los datagramas llegan enteros o no llegan, y pueden llegar desordenados. Funciona igualmente, por dos bytes que el tramado ha llevado desde siempre.

Un datagrama perdido cuesta una trama, y el siguiente byte de bandera recupera el enlaceLas fronteras no coinciden, y da igualTRAMAS PPP TAL COMO LAS ESCRIBIÓ pppd7Etrama A + FCS7Etrama B + FCS7Etrama C + FCS7Etrama D + FCSLO QUE EL PORTADOR ENVIÓ DE VERDAD — netcat lee un búfer, no una tramadatagrama 1datagrama 2 — perdidodatagrama 3datagrama 4La trama B pierde su parte central, así que su suma de verificación falla y se descarta. La trama C pierde sus primeros bytes y corre igual suerte.Dos tramas perdidas son dos paquetes IP perdidos. La capa de encima los retransmite, y lo hace sabiendo que el camino perdió algo —que es precisamente la señal que un portador TCP habría escondido entregándolos tarde.
Netcat lee lo que haya en el búfer y lo escribe en un datagrama, así que las fronteras de trama y las de datagrama no tienen nada que ver entre sí. Pierde un datagrama y el receptor ve una trama a la que le faltan bytes: la secuencia de verificación de trama falla y la trama se descarta, exactamente como haría en una línea serie ruidosa. El siguiente 0x7E resincroniza el flujo. Una trama descartada cuesta un paquete, y la capa de encima lo retransmite — que es la señal de pérdida que el TCP interior necesitaba y nunca tuvo sobre un portador TCP.

PPP sobre HDLC asíncrono se diseñó para una línea que corrompe bytes. Cada trama lleva una secuencia de verificación de trama de 16 bits; una trama que falla se descarta, y el siguiente byte de bandera resincroniza al receptor. El desorden es más raro que la pérdida y produce el mismo resultado: una FCS mala, una trama descartada, una resincronización.

Así que un datagrama perdido cuesta una trama PPP — un paquete IP, la condición normal de toda red que se ha construido jamás. El dueño del paquete retransmite, y el control de congestión ve una pérdida real y responde correctamente. Ese es el argumento entero a favor de UDP: deja que el tráfico de dentro del túnel se entere de la verdad sobre el camino.

Eso sí, no dejes que las tramas crezcan hasta necesitar que el portador las fragmente. Un fragmento perdido mata entonces el datagrama entero y tu tasa de pérdida efectiva se multiplica. Mantén la MTU de PPP bien por debajo de la MTU del camino.

Direcciones, Rutas, Y Hacer IPv6 Como Es Debido

ppp0 es una interfaz punto a punto — sin subred, sin ARP — así que local:remoto es todo el direccionamiento y enrutas por encima de forma explícita. Para un solo equipo que alcanza una red detrás del extremo lejano:

# en el cliente, una vez levantado el enlace
ip route add 203.0.113.0/24 via 192.0.2.1 dev ppp0

Para que el extremo lejano reenvíe por cuenta del cliente, los dos pasos de siempre:

sysctl -w net.ipv4.ip_forward=1
nft add rule inet nat postrouting oifname "eth0" ip saddr 192.0.2.2/32 masquerade

proxyarp hará que el servidor responda al ARP del cliente en su propio segmento4, lo cual está bien hasta que un segundo cliente quiere lo mismo. Haz entonces la parte que casi todo el mundo se salta. Ejecuta IPv6 por encima — un enlace punto a punto sin NAT, sin dominio de difusión y sin escasez de direcciones es el sitio más fácil de tu red para hacer IPv6 como es debido:

pppd nodetach noauth local \
     +ipv6 ipv6 ::1,::2 \
     nodefaultroute noipdefault \
     192.0.2.2:192.0.2.1 \
     pty 'ncat --udp 198.51.100.10 443'

+ipv6 habilita IPV6CP; ipv6 <local>,<remoto> fija los dos identificadores de interfaz de 64 bits4, el enlace se levanta con enlace-local, y le pones un /64 global encima7. IPV6CP es independiente de IPCP, así que noip no lleva más que IPv6 — algo razonable de construir en 2026, en una sola palabra.

Mantenerlo En Pie Cuando El Portador Muere En Silencio

Este es el fallo que se come una tarde, así que le toca su propia sección.

Un portador TCP que muere mal — el extremo lejano apagado, una entrada de tabla NAT caducada, una caja intermedia que dejó de reenviar — no se cierra. No hay FIN, ni RST, ni nada. Netcat se queda ahí sujetando un socket que no entregará otro byte jamás, pppd se queda ahí sujetando un pty que no verá otra trama jamás, e ip link informa tan contento de que ppp0 está UP. Un portador UDP no tiene estado de conexión ninguno, así que nunca se entera de nada.

PPP trae la respuesta incorporada, y viene desactivada:

lcp-echo-interval 10 lcp-echo-failure 3

Eso manda un eco LCP cada diez segundos y tira el enlace después de tres sin respuesta4 — treinta segundos para pillar un portador muerto, en exactamente el caso que nombra la página de manual, «no hardware modem control lines»4, que es todo pty que se haya fabricado. Después decide qué pasa a continuación:

persist maxfail 0 holdoff 5
Detectar un portador muerto en treinta segundos, y reconstruirloUn portador puede morir sin cerrarse. Así se entera PPP, y así se recupera.El portador muere en silencioextremo lejano apagado · entrada NAT ida ·la caja intermedia deja de reenviarNada lo informasin FIN, sin RST · ip link siguediciendo ppp0 UP · UDP no tiene estadoEl detectorlcp-echo-interval 10 lcp-echo-failure 3eco cada 10 s, caído tras 3 fallidos — 30 sLa reconstrucciónpersist maxfail 0 holdoff 5reiniciar, no rendirse, 5 s entre intentosUn solo supervisorunidad systemd, Restart=always,pppd reejecuta el comando de pty —netcat y socket nuevos con élPon el eco en los dos extremos. Un eco solo demuestra el camino por el que volvió la respuesta.Pon `ip route` en /etc/ppp/ip-up.d/ y las rutas IPv6 en /etc/ppp/ipv6-up.d/, para que el enrutado se vuelva a aplicar cada vez que el enlace regresa —y no puesto una vez a mano y perdido en la primera reconexión.
lcp-echo-interval 10 lcp-echo-failure 3 es el detector: un eco cada diez segundos, enlace caído tras tres fallidos. persist maxfail 0 holdoff 5 es la reconstrucción: reiniciar, no rendirse nunca, esperar cinco segundos para que un camino inestable no sea una bomba de bifurcación — y pppd vuelve a ejecutar el comando de pty, así que vienen con él un netcat y un socket nuevos. Pon el eco en los dos extremos; un solo supervisor, una unidad de systemd con Restart=always, gana a dos procesos discutiendo. Pon ip route en /etc/ppp/ip-up.d/ para que el enrutado vuelva con el enlace.

No Tiene Cifrado Ni Autenticación

Todo lo de arriba es un túnel que funciona. No es uno seguro, y el hueco no es un detalle.

No hay cifrado. No es cifrado débil. Es ninguno. Cada paquete que metas por aquí va en claro por el cable, envuelto en una trama HDLC que cualquier herramienta de captura descodifica al verla. tcpdump te enseñará el contenido del túnel de otro con la misma facilidad que el tuyo.

No hay autenticación del par. noauth lo dice. Un servicio de escucha netcat acepta lo que llegue al puerto: la primera conexión o el primer datagrama desde donde sea. Quien llegue primero se lleva un enlace enrutado hacia dentro de tu red. PPP sí tiene autenticación (PAP en claro, CHAP como desafío-respuesta8), y las dos demuestran quién es el par sin cifrar ni un byte de lo que viene después: CHAP aquí te da un túnel que sabe con quién habla y sigue publicando el contenido a cualquiera que esté en el camino.

Hay una opción de cifrado en la familia PPP — MPPE9, el módulo que está en ppp_mppe.ko. No eches mano de él: es RC4 con clave derivada del intercambio MS-CHAPv2, roto en público desde hace más de una década, y la razón de que PPTP esté muerto. Construir un túnel nuevo sobre él en 2026 elige un cifrador roto conocido por encima de uno que funciona y no cuesta nada.

Así que el resumen honesto hasta aquí: un enlace enrutado sin confidencialidad y sin control de acceso. Bien para un laboratorio, bien dentro de un enlace que ya está cifrado, mal en cualquier otro sitio. El arreglo es cifrar el portador — las dos secciones siguientes, y la razón para usar ncat en vez del netcat que traiga tu distribución.

Fase Tres: Envolver El Portador En TLS

La respuesta limpia deja pppd exactamente como está y sustituye el portador por uno que hace TLS. pppd no se entera nunca de que algo cambió.

Primero el certificado. Uno autofirmado basta mientras el cliente lo verifique. Una sesión TLS sin verificar es una conversación cifrada con alguien a quien no has identificado, y eso no detiene a nadie:

openssl req -x509 -newkey rsa:4096 -days 825 -nodes \
  -keyout tunnel.key -out tunnel.crt \
  -subj "/CN=tunnel.example.net" \
  -addext "subjectAltName=DNS:tunnel.example.net"

Ncat

Ncat trae TLS incorporado. También encadena a través de un proxy, --proxy equipo:puerto --proxy-type http|socks4|socks510, así que el portador puede terminar en un relé en vez de en el extremo lejano del túnel. Ese es el punto sobre el que gira la sección de la salida. Extremo que escucha:

pppd nodetach noauth local passive \
     nodefaultroute noipdefault 192.0.2.1:192.0.2.2 \
     lcp-echo-interval 10 lcp-echo-failure 3 mtu 1400 mru 1400 \
     pty 'ncat --listen --keep-open --ssl --ssl-cert tunnel.crt --ssl-key tunnel.key 443'

Extremo que conecta:

pppd nodetach noauth local \
     nodefaultroute noipdefault 192.0.2.2:192.0.2.1 \
     lcp-echo-interval 10 lcp-echo-failure 3 mtu 1400 mru 1400 \
     pty 'ncat --ssl --ssl-verify --ssl-trustfile tunnel.crt tunnel.example.net 443'

--ssl-verify es la palabra que importa. Sin ella, --ssl te da cifrado frente a quien escucha de forma pasiva y nada frente a quien conteste el puerto primero; con ella, Ncat verifica la confianza y el nombre de dominio contra el fichero de confianza11. Eso sí, Ncat no comprueba certificados de cliente en el lado servidor, así que el servidor no puede identificar al cliente. Combínalo con CHAP, o usa uno de los dos siguientes.

Stunnel

El envoltorio tradicional, y el que hace la autenticación mutua como es debido:

[ppp]
accept  = 443
connect = 127.0.0.1:6001
cert    = /etc/stunnel/tunnel.crt
key     = /etc/stunnel/tunnel.key
CAfile  = /etc/stunnel/clients.crt
verify  = 2

verify = 2 exige un certificado de cliente firmado por una CA que esté en CAfile — el control de acceso que la construcción con netcat nunca tuvo. El cliente ejecuta stunnel en modo cliente, y el comando de pty de pppd conecta al lado local en claro.

Socat

socat hace el asunto entero en un proceso por extremo, con la verificación activada por defecto:

# extremo que escucha
pppd ... pty 'socat - OPENSSL-LISTEN:443,reuseaddr,cert=tunnel.pem,cafile=clients.crt,verify=1'

# extremo que conecta
pppd ... pty 'socat - OPENSSL:tunnel.example.net:443,cafile=tunnel.crt,verify=1'

socat no está instalado en la máquina en la que se escribió esto, así que eso sale de su documentación — comprueba tus propias banderas. Es el que yo cogería, porque es el único de los cuatro que además habla DTLS.

Openssl, cuando no hay otra cosa

openssl está en toda máquina que tenga TLS, y s_server/s_client llevarán una tubería:

# extremo que escucha
pppd ... pty 'openssl s_server -quiet -accept 443 -cert tunnel.crt -key tunnel.key'

# extremo que conecta
pppd ... pty 'openssl s_client -quiet -verify_return_error -CAfile tunnel.crt -connect tunnel.example.net:443'

-quiet calla el cartel que si no acabaría dentro de tu flujo PPP, y -verify_return_error hace que un fallo de verificación cierre la conexión en vez de avisar y seguir. Son herramientas de depuración portándose como tales, pero en una máquina donde no puedes instalar nada te levantan un enlace.

A secas, TLS sobre TCP, o DTLS sobre UDPLa misma mitad de enlace en los dos extremos. Solo cambia el medio.pppd o tap0netcat a secas, UDPncat --udp --listen 6000pppd o tap0En claro por el cable, y quien escucha coge a quien llegue primero al puerto. Sin confidencialidad, sin control de acceso.pppd o tap0TLS sobre TCP — ncat, stunnel u opensslncat --ssl --ssl-verify --ssl-trustfile tunnel.crt host 6000pppd o tap0Protegido, y el certificado dice quién es el extremo lejano. Pero el portador vuelve a ser TCP — y es la única forma que comprime en flujo.pppd o tap0DTLS sobre UDP — socat u opensslsocat - OPENSSL-DTLS-CLIENT:host:6000,cafile=tunnel.crt,verify=1pppd o tap0Cifrado, autenticado, y todavía datagramas. Un paquete perdido sigue perdido en vez de dos pilas discutiendo. Apunta aquí.
El mismo pppd en los dos extremos en todo momento — solo cambia el comando de pty. Netcat a secas te da un enlace sin nada que lo proteja. TLS sobre TCP protege los bytes y reintroduce el problema del TCP portador. DTLS sobre UDP es la forma a la que apuntar: cifrado, autenticado, y todavía un portador de datagramas, así que un paquete perdido sigue siendo un paquete perdido en vez de convertirse en una pelea de retransmisiones entre dos pilas.

Fase Cuatro: DTLS, Porque El Portador Debería Seguir Siendo UDP

Aquí viene la parte incómoda, y la razón de que esta sección vaya aparte. Todas las opciones de TLS de arriba van sobre TCP, así que envolver el portador en TLS deshace el argumento a favor de UDP y te devuelve el derretimiento. El cifrado y el transporte correcto no deberían ser un trueque. DTLS es TLS sobre datagramas, y es lo que quieres: conserva la protección de la capa de registro, tira las garantías de orden y retransmisión, y deja perdidos los paquetes perdidos, que es exactamente lo que la secuencia de verificación de trama de PPP está hecha para absorber.

Ncat no puede. socat y openssl sí:

# extremo que escucha
pppd ... pty 'socat - OPENSSL-DTLS-LISTEN:443,cert=tunnel.pem,cafile=clients.crt,verify=1'

# extremo que conecta
pppd ... pty 'socat - OPENSSL-DTLS-CLIENT:tunnel.example.net:443,cafile=tunnel.crt,verify=1'

Y con openssl a solas, donde -dtls elige cualquier versión de DTLS:

# extremo que escucha
pppd ... pty 'openssl s_server -quiet -dtls -accept 443 -cert tunnel.crt -key tunnel.key'

# extremo que conecta
pppd ... pty 'openssl s_client -quiet -dtls -verify_return_error -CAfile tunnel.crt -connect tunnel.example.net:443'

Vigila el tamaño de la trama. Un registro DTLS no se puede fragmentar como un registro TLS se reparte por un flujo TCP, así que todo tiene que caber de una vez en la MTU del camino.

La pila de sobrecarga, y la MTU interior que sobraBaja desde la MTU del camino y usa lo que quedecabecera IP20 (v4) / 40 (v6)cabecera UDP8registro DTLS + etiqueta≈ 30tramado PPP / Ethernetunos pocosMTU interior — lo que el túnel puede llevar: apunta a 1400, suelo 1280Un registro DTLS no se puede fragmentar como un registro TLS se reparte por un flujo TCP, así que todo esto tiene que caber de una vez en la MTU del camino.La misma forma que OpenVPN desde 2001 — un portador de datagramas, DTLS, una interfaz virtual encima. No es excéntrico; solo está desmontado.
Baja desde 1500: 20 bytes de cabecera IPv4 o 40 de IPv6, 8 de UDP, unos 30 para el registro DTLS y su etiqueta, unos pocos para el tramado, y el resto es la MTU interior que puede llevar el túnel. Apunta a 1400 en un camino normal; 1280 es el suelo seguro si algo por el medio es a su vez un túnel. Esta forma — un portador de datagramas, DTLS, una interfaz virtual encima — es casi lo que hace OpenVPN desde 2001. No es excéntrica; solo está desmontada.

Ajustar La Construcción PPP: MTU, Compresión, Y Qué Ayuda De Verdad

Cuatro mandos, todos opciones de pppd — la construcción con tap de la sección siguiente no tiene ninguno, porque no tiene nada de la maquinaria que configuran.

Cuatro mandos de pppd: uno que poner, uno que dejar, dos que apagarUno que poner, uno que dejar, dos que apagarPONLOMTU y MRULos dos extremos, con margen: 1400 a secas, 1280 bajo un túnel. Una trama fragmentada pierde un paquete entero.DÉJALAACCMYa está a cero por defecto, que es lo correcto en un socket limpio. Tócala solo si algo se come los caracteres de control.APAGAnovjCompresión de cabeceras de Van JacobsonAhorra un error de redondeo en un enlace rápido, cuesta CPU por paquete y se rompe bajo pérdida. Apagada por encima del módem.APAGAnodeflate nobsdcompLa carga ya es TLS/DTLS — comprimir texto cifrado es trabajo puro, y a través de una frontera de seguridad, un ataque.Ninguno de estos hace el túnel más rápido. PPP no es el cuello de botella — PPPoE hace 2 Gbit con el mismo demonio, porque su camino de datosse queda en el núcleo. El coste aquí es el pty: cada byte cruza al espacio de usuario y vuelve. Eso es del pseudoterminal, no de PPP.
La MTU y la MRU son las que importan: ponlas en los dos extremos con margen, porque una trama que el portador tenga que fragmentar pierde un paquete entero con cualquier descarte. La ACCM ya está a cero y correcta en un socket limpio. Apaga la compresión de cabeceras de Van Jacobson con novj por encima de la velocidad de un módem. Ahorra un error de redondeo, cuesta CPU por paquete y se rompe bajo pérdida. Apaga deflate/bsdcomp: la carga ya está cifrada, y comprimir a través de una frontera de seguridad es un ataque, no una función.

Nada de eso hace el túnel más rápido, y la razón importa, porque a PPP se le culpa de ello y no debería. PPP no es el cuello de botella. PPPoE lleva 2 Gbit con el mismo demonio, porque su camino de datos no sale nunca del núcleo — ppp_generic y pppoe hacen el tramado y el reenvío, y pppd solo se ocupa del plano de control. Lo que te cuesta aquí es el pty: cada byte cruza al espacio de usuario, atraviesa netcat, entra en un socket y vuelve, un viaje de ida y vuelta que PPPoE no hace nunca. Eso es del pseudoterminal, no de PPP.

Que es un buen momento para mirar la otra forma de hacer esto, donde no hay pty ninguno.

La Otra Forma: Un Dispositivo Tap, Y Nada De PPP

Todo hasta aquí ha usado PPP para la mitad del enlace. Hay una segunda forma de fabricar una interfaz virtual, y no necesita protocolo ninguno.

El controlador TUN/TAP del núcleo te entrega una interfaz y un descriptor de fichero atados entre sí: escribe un paquete en el descriptor y aparece en la interfaz como si viniera de un cable; lee y obtienes un paquete que el núcleo quería mandar. Esa es la interfaz entera12, y está en Linux desde 1999.

ip tuntap add dev tap0 mode tap user damien group damien
ip link set tap0 mtu 1400 up
ip addr add 192.0.2.1/30 dev tap0
Un tap es una interfaz por un lado y un descriptor de fichero por el otroSin demonio, sin protocolo, sin pseudoterminal. Un descriptor de fichero.NÚCLEOtap0una interfaz normal,con una dirección y una ruta/dev/net/tunTUNSETIFF nombrael dispositivo que obtienestu procesosocat, o cincuenta líneasde Python sujetando el fdsocket UDPuna trama por datagramala redy el extremo lejanouna lectura = una tramaLo que falta frente a la construcción PPP: sin tamaño de trama negociado, sin comprobación de vida, sin direcciones intercambiadas, sin autenticación.Configuras los dos extremos a mano y nunca hablan de ello. Nada vigila el enlace, así que nada te dirá que se murió.
Sin demonio y sin protocolo. El núcleo presenta tap0 como una interfaz normal y entrega el otro lado al proceso que abrió /dev/net/tun. Una lectura devuelve exactamente una trama Ethernet; una escritura inyecta exactamente una. Todo lo que pppd negociaba — direcciones, tamaños de trama, comprobación de vida — ahora lo configuras a mano en los dos extremos, y los dos extremos no hablan de nada de eso entre sí.

Dos detalles de ese primer comando valen más de lo que parecen.

user damien hace el dispositivo persistente y sin privilegios. Creado así sobrevive al proceso que lo usa, y un usuario nombrado puede abrirlo sin ser root. Root crea el dispositivo una vez; la cosa que palea tramas no necesita root para nada. Guarda esa idea para la sección de la salida.

mode tun es la otra mitad del mismo controlador, y es la que casi todo el mundo quiere en realidad. Tun lleva paquetes IP. Tap lleva tramas Ethernet. La diferencia importa lo bastante como para tener su propia sección abajo.

Lo que cedes frente a PPP es todo lo que PPP negocia: sin LCP, así que sin tamaño de trama acordado ni comprobación de vida; sin IPCP/IPV6CP, así que los dos extremos se configuran a mano; sin autenticación, sin compresión de cabeceras. Un dispositivo tap es un agujero en el núcleo, y el protocolo que va por él es el que tú pongas. Lo que ganas es cero sobrecarga de tramado, cero escapes, cero canal de control, y una interfaz que se puede puentear.

Tap Sobre UDP, Donde Un Datagrama Es Una Trama

Esta es la correspondencia más limpia del artículo entero, y sale sola del diseño. Un dispositivo tap es un dispositivo de datagramas — un read() devuelve exactamente una trama — y un socket UDP es un socket de datagramas, un sendto() por datagrama. Así que una trama entra en un datagrama, llega como trama, y no hay nada que delimitar, almacenar ni resincronizar. Pierde un datagrama y has perdido una trama, que es como se ve un paquete descartado de todos modos.

Con socat, cada extremo es un comando:

# extremo que escucha
socat TUN:192.0.2.1/30,tun-type=tap,tun-name=tap0,iff-up UDP-LISTEN:443

# extremo que conecta
socat TUN:192.0.2.2/30,tun-type=tap,tun-name=tap0,iff-up UDP:198.51.100.10:443

socat no está instalado en la máquina en la que se escribió esto, así que esos dos salen de su documentación y no de una ejecución aquí — comprueba los nombres de dirección de tu propia versión antes de fiarte. Merece la pena tenerlo: es la única herramienta de este artículo que hace tun, tap, TLS y DTLS en un solo proceso.

Donde no puedas instalar nada, el trabajo entero son unas cincuenta líneas sin más dependencias que la biblioteca estándar. El corazón, con IFF_NO_PI apagando la cabecera de cuatro bytes que el controlador antepondría si no:

TUNSETIFF, IFF_TAP, IFF_NO_PI = 0x400454CA, 0x0002, 0x1000   # IFF_TUN is 0x0001

def open_tap(name):
    fd = os.open("/dev/net/tun", os.O_RDWR)
    fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", name.encode(), IFF_TAP | IFF_NO_PI))
    return fd

# ... learn the peer from the first datagram (UDP has no accept), then shovel:
while True:
    ready, _, _ = select.select([tap, sock], [], [])
    if tap in ready and peer:
        sock.sendto(os.read(tap, MTU), peer)     # one read = one frame = one datagram
    if sock in ready:
        frame, src = sock.recvfrom(MTU)
        peer = src                               # last speaker wins — see the note below
        if frame:
            os.write(tap, frame)

El fichero completo — el análisis de argumentos, IPv6 con getaddrinfo, el datagrama de longitud cero que le dice a un servicio de escucha UDP dónde responder, y la compresión que añade la sección siguiente — está en el paquete:

tapcat.py — el asunto entero, unas cien líneas tapcat.py · 6 kB

peer = src en cada datagrama es la parte que hay que leer dos veces. Quien mandó la última trama se convierte en el par. Cómodo detrás de un NAT cuyo puerto de origen no para de moverse, y una puerta abierta en una red no confiable, donde cualquiera que pueda mandar un datagrama al puerto se queda con el túnel. Bien dentro de una sesión DTLS, que es a donde va esto; mal por sí solo. La mitad del socket del script se probó aquí sobre loopback IPv6: llega quien abre, quien escucha aprende el par, una trama cruza y vuelve la respuesta; la mitad del tap necesita root, la única parte que no pude ejecutar.

Sobre TCP Tienes Que Inventarte El Tramado

Cambia ahora el portador por TCP y mira cómo aparece un problema entero que PPP resolvió en silencio en 1994.

TCP es un flujo de bytes sin fronteras de registro y sin promesa alguna sobre cómo se agrupan los bytes al llegar: dos tramas escritas seguidas pueden llegar en una lectura, o una trama en tres. El receptor tiene un montón de bytes sin idea de dónde acaba una trama, y un dispositivo tap solo acepta tramas enteras.

Un datagrama por trama, o un prefijo de longitud que hay que inventarseUna lectura de un tap es una trama. Mantenerlo así es trabajo del portador.SOBRE UDP — la frontera sale gratisdatagrama = trama Adatagrama = trama Bdatagrama = trama Cdatagrama = trama DUna lectura, un datagrama, una escritura en el extremo lejano. Nada que delimitar, nada que almacenar, nada que resincronizar tras una pérdida.SOBRE TCP — las fronteras desaparecen y hay que devolverlasun solo flujo de bytes — dos tramas pueden llegar en una lectura, una trama en treslentrama Alentrama Blentrama Clentrama DDos bytes de longitud en big-endian delante de cada trama, y un receptor que lee la longitud y después exactamente esos bytes.Funciona, y no se recupera. HDLC se resincroniza en el siguiente 0x7E porque una bandera no es ambigua; un flujo con prefijos quepierde el sitio lee todas las longitudes siguientes de en medio de una trama. Añade un marcador y una suma y habrás rehecho HDLC.
Sobre UDP, una trama es un datagrama y la frontera sale gratis. Sobre TCP las fronteras desaparecen, así que quien envía tiene que añadir un prefijo de longitud a cada trama y quien recibe tiene que reensamblar a partir de él. PPP no tiene este problema porque trae su propio tramado — un byte de bandera en cada punta y una suma de verificación — que es también lo que le permite resincronizar tras un daño. Un flujo con prefijos de longitud no puede: pierde el paso un byte y todas las tramas siguientes salen mal.

Así que sobre TCP escribes tu propio tramado. Dos bytes de longitud en big-endian delante de cada trama es la respuesta habitual:

# sending
sock.sendall(struct.pack("!H", len(frame)) + frame)

# receiving
def recv_exactly(sock, n):
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("carrier closed")
        buf += chunk
    return buf

length = struct.unpack("!H", recv_exactly(sock, 2))[0]
frame  = recv_exactly(sock, length)

Eso funciona, y es estrictamente peor que la versión UDP. Es el problema de TCP sobre TCP otra vez; añade dos bytes y un bucle de reensamblado por trama; y no tiene vuelta atrás desde un error, porque un flujo con prefijos de longitud que pierde el sitio lee cada longitud siguiente de en medio de una trama. HDLC resincroniza en el siguiente 0x7E; esto no puede, salvo que reinventes HDLC peor. Tercer argumento a favor de UDP, y el más fuerte: sobre un portador de datagramas no hay problema de tramado, porque el portador ya trae la única función que necesitabas.

Tun O Tap: Capa 3 Salvo Que Necesites De Verdad Capa 2

El mismo controlador te da dos dispositivos, y la gente elige el equivocado sin parar porque un tutorial dijo tap.

tuntap
Qué cruzapaquetes IPtramas Ethernet
Sobrecarga por paqueteningunacabecera Ethernet de 14 bytes
ARP, DHCP, difusiónnosí, todo, por el túnel
Protocolos que no son IPnosí
Puede unirse a un puentenosí
Equivale aun enlace punto a punto, como ppp0un cable de red

Tun es un enlace enrutado — como la interfaz PPP de la primera mitad: dos direcciones, una ruta, paquetes que entran y salen. Tap es un cable Ethernet virtual, así que cada difusión, cada consulta ARP y cada trozo de ruido multicast del segmento cruza ahora tu túnel y quema ancho de banda.

Cambia una línea del script para cambiar de uno a otro:

IFF_TUN = 0x0001            # instead of IFF_TAP

Usa tap cuando necesites de verdad capa 2. Hay razones reales: un protocolo que no es IP, un latido de clúster que espera ver difusiones, un servidor DHCP que tiene que alcanzar clientes al otro lado del túnel, o puentear dos segmentos en uno.

Esto último es el caso habitual y con el que hay que tener cuidado:

ip link add br0 type bridge
ip link set tap0 master br0
ip link set eth1 master br0
ip link set br0 up

Ahora el segmento remoto es parte del tuyo local — sus difusiones, su árbol de expansión, su barullo de MAC y, si alguien fue descuidado, su servidor DHCP. Puentear dos sedes que las dos usan 192.168.1.0/24 es una mala tarde; una que da la vuelta al mismo segmento es una mala semana. Usa tun por defecto, echa mano de tap cuando puedas nombrar la cosa de capa 2 que necesitas, y puentea solo después de mirar qué está difundiendo en los dos lados.

Los Mismos Envoltorios, Un Proceso Por Extremo

La construcción con tap tiene el mismo agujero que la de PPP: el portador va en claro y quien escucha acepta a quien llegue primero. El arreglo es el mismo, y con socat se reduce a un comando por extremo, porque fabrica el dispositivo y termina la sesión DTLS en un solo proceso:

# extremo que escucha
socat TUN:192.0.2.1/30,tun-type=tap,tun-name=tap0,iff-up \
      OPENSSL-DTLS-LISTEN:443,cert=tunnel.pem,cafile=clients.crt,verify=1

# extremo que conecta
socat TUN:192.0.2.2/30,tun-type=tap,tun-name=tap0,iff-up \
      OPENSSL-DTLS-CLIENT:tunnel.example.net:443,cafile=tunnel.crt,verify=1

Esa es la construcción correcta más corta de este artículo. Una interfaz virtual, un portador de datagramas, autenticación mutua por certificado y cifrado. Dos comandos. Sin demonio, sin negociación de protocolo.

verify=1 no es opcional — el mismo punto que con --ssl-verify, y con el comportamiento de peer = src de arriba, una parte sin identificar está a un datagrama de adueñarse de tu túnel. Si te has quedado atado al script de Python, no le atornilles TLS: apúntalo a un puerto de loopback y pon el envoltorio delante, o usa socat. Un envoltorio TLS hecho a mano alrededor de un túnel hecho a mano son dos oportunidades de equivocarse en la parte interesante.

Fase Cinco: Comprimir El Flujo Con zstd

Hay una cosa más que merece la pena poner en la trastienda, y a diferencia de las opciones de compresión de pppd esta sí puede pagar: comprimir las tramas con zstd antes de que entren en el portador.

La palabra importante ahí es tramas, en plural. Comprime el flujo, no cada paquete por su cuenta. Es el mayor efecto medido de este artículo.

El tráfico de red es repetitivo de una forma que solo se ve entre paquetes — las mismas cabeceras, los mismos nombres de equipo, las mismas claves JSON, una y otra vez. Un compresor que empieza de cero en cada trama de 1400 bytes no ve nada de eso; uno que mantiene su ventana entre tramas lo ve todo.

Comprime antes de cifrar, y conserva la ventana si el portador te dejaEl orden es fijo. El modo es la decisión.tap0una lectura, una tramacomprimirzstd, nivel 1cifrarDTLS, o TLSportadorUDP, o TCPNunca alrevés.FLUSH_BLOCK — conservar la ventanaEmite todo lo acumulado, conserva el historial. Cada tramase codifica contra todas las anteriores. Sin latencia añadida:una trama entra, una trama sale.5.0%con tráfico repetitivo · 7,5 % en líneas de registroNecesita cada trama entregada, en orden.Así que: TCP, o TLS sobre TCP. No UDP, no DTLS.FLUSH_FRAME — tirarla en cada paqueteCada datagrama es una trama zstd completa y se descodificasola, así que el datagrama N funciona aunque 1 a N-1 se pierdan.El único modo que puede usar un portador de datagramas.14.8%con el mismo tráfico · 24,2 % en líneas de registroUn diccionario entrenado recupera casi todo eso:5,3 % y 15,1 %, y sigue tolerando la pérdida.
La compresión va entre el dispositivo tap y el portador, y antes del cifrado, porque el texto cifrado no se comprime. FLUSH_BLOCK es el modo que importa: emite todo lo acumulado, así que una trama que entra da una trama que sale sin latencia añadida, mientras conserva el historial de compresión para la trama siguiente. FLUSH_FRAME tira ese historial en cada paquete, que es lo que lo hace seguro sobre un portador con pérdida y lo que lo hace tres veces peor en el cable.

En una Fedora actual esto no necesita instalar nada — Python 3.14 trajo zstd a la biblioteca estándar13; esta máquina tiene la 3.14.7 contra zstd 1.5.7:

from compression.zstd import ZstdCompressor, ZstdDecompressor   # Python 3.14+

_c, _d = ZstdCompressor(level=1), ZstdDecompressor()

# FLUSH_BLOCK: emit everything so far, keep the window for the next frame.
out   = _c.compress(frame, mode=ZstdCompressor.FLUSH_BLOCK)
frame = _d.decompress(out)

Un compresor por sentido, vivo durante toda la vida del enlace. Un FLUSH_BLOCK por trama, así que una trama sale en el momento en que llega sin nada almacenado, y el extremo lejano devuelve una trama por bloque.

Lo Que Vale La Ventana, Medido

Cifras de esta máquina. Las mismas tramas de 1400 bytes, el mismo nivel 1, y la única diferencia es si el compresor conserva su historial:

Tráfico en el túnelflujo, FLUSH_BLOCKpor paquete, FLUSH_FRAMEpor paquete con diccionario entrenado
Llamadas de API repetitivas y telemetría5,0 %14,8 %5,3 %
Líneas de registro7,5 %24,2 %15,1 %
Texto plano y ficheros de configuración37,8 %51,3 %45,3 %
Bytes aleatorios, haciendo de TLS100 %+100,7 %—

El rendimiento al nivel 1 dio 205 MB/s con texto y más de 1 GB/s con el tráfico repetitivo — cuanto más comprimible, más rápido, porque hay menos que codificar.

El tráfico de registro baja al 7,5 % de su tamaño original, frente al 24,2 % por paquete — el triple, los mismos datos, el mismo nivel, por una sola bandera. Y el nivel 1 es el nivel: el nivel 3 compró alrededor de un uno por ciento, el nivel 9 otro más mientras bajaba el rendimiento de 211 MB/s a 61.

La Pega, Y Es La Misma Pega Que Todo Lo Demás Aquí

Una ventana compartida significa que cada trama depende de las anteriores: pierde una y el historial del descompresor deja de coincidir, y nada posterior se descodifica. Así que la compresión de flujo necesita un portador que lo entregue todo en orden — TCP, o TLS sobre TCP, no UDP ni DTLS. Ese es el único argumento honesto a favor del portador TCP en todo el artículo. Si lo que cruza tu túnel es de verdad comprimible — syslog, replicación de base de datos en claro, telemetría, una API charlatana — una sesión TLS con compresión de flujo mueve un tercio de los bytes que movería un portador de datagramas, cosa que en un camino decente puede ganarle a la penalización por retransmisión. Mídelo con tu propio tráfico.

Sobre UDP, Donde Un Diccionario Hace El Trabajo De La Ventana

Donde el portador es UDP o DTLS, y debería serlo por defecto, no puedes mantener una ventana: cada datagrama se sostiene solo, lo que significa FLUSH_FRAME y la columna más floja de arriba. Un diccionario entrenado le da al compresor el contexto entre paquetes que habría tenido una ventana, sin dependencia ninguna entre datagramas:

# capture a few thousand real frames off the link first, one per file
zstd --train frames/* -o tunnel.dict --maxdict=110000
```[^zstddict]

```python
from compression.zstd import ZstdCompressor, ZstdDecompressor, ZstdDict
d = ZstdDict(open("tunnel.dict", "rb").read())
_c = ZstdCompressor(level=1, zstd_dict=d)      # load it ONCE, not per frame
out = _c.compress(frame, mode=ZstdCompressor.FLUSH_FRAME)

Con el tráfico repetitivo eso llevó la compresión por paquete del 14,8 % al 5,3 %, recuperando el 96 % de la distancia hasta la compresión de flujo completa sin dejar de tolerar la pérdida; con líneas de registro el 55 % de la distancia, con texto general el 44 %.

Vienen tres reglas con ello. Los dos extremos tienen que cargar el mismo diccionario o no se descodifica nada — comprobado aquí, una trama comprimida con diccionario lanza ZstdError sin él. Entrena con una captura del tráfico real, porque un diccionario es una apuesta previa y una equivocada te cuesta: el diccionario de texto empeoró ligeramente otro texto. Y cárgalo una vez en un compresor de vida larga; pasarlo en cada llamada midió 5 MB/s, y no es una errata.

El Byte De Cabecera, Y La Rampa De Un Segundo

Dos cosillas que impiden que esto sea frágil.

Cada datagrama lleva una cabecera de un byte, y nombra el modo en vez de decir solo «comprimido»: 0x00 la trama tal cual, 0x01 una trama zstd autónoma, 0x02 un bloque de un flujo continuo. Un receptor puede entonces descodificar lo que haya elegido el extremo lejano sin estar configurado para coincidir, cosa que ya vale el byte por sí sola.

En modo trama, quien envía solo manda la forma comprimida cuando de verdad es más pequeña, porque comprimir no siempre gana: los bytes aleatorios salieron al 100,7 %, y un ACK de TCP de 64 bytes se comprime a 73 — los diez bytes de cabecera de zstd sobre un paquete sin nada que apretar. El recuento de paquetes en un enlace real lo dominan los paquetes pequeños, así que sin esa comprobación inflarías la mayoría de tu tráfico para encoger la minoría. En modo flujo manda siempre la forma comprimida, porque saltarse una dejaría las dos ventanas fuera de paso.

Y el enlace empieza en crudo: durante el primer segundo cada trama sale sin comprimir, digan lo que digan los ajustes, y cada sentido arranca por su cuenta:

RAMP = 1.0     # seconds of raw frames before compression starts

def pack(self, frame):
    if self.c is None or time.monotonic() - self.started < RAMP:
        return RAW + frame
    out = self.c.compress(frame, mode=ZstdCompressor.FLUSH_BLOCK)
    return ZSTD + out if len(out) < len(frame) else RAW + frame

Esa es la idea de las fases del principio del artículo aplicada a un solo enlace. El túnel se levanta por el camino más simple que tiene, demuestra que puede llevar una trama, y solo entonces empieza a hacer algo listo. Cuando se rompa, sabes en qué segundo se rompió.

Fase Seis: Comprimido Y Cifrado, Y Lo Que Cuesta El Orden

El orden importa, y solo uno funciona: comprimir, después cifrar. El texto cifrado no se comprime, como enseña la fila del 100,7 % — que es también por lo que TLS 1.3 quitó su propia compresión, dejando tu capa como el único sitio donde hacerlo. Ese orden tiene un problema conocido, el mismo que le puse a deflate: comprimir antes de cifrar filtra el texto plano a través de la longitud del texto cifrado, y donde un atacante puede inyectar datos elegidos junto a un secreto y mirar los tamaños, esa filtración ha sido un ataque que funciona más de una vez — CRIME y BREACH contra TLS14, y VORACLE contra exactamente esta forma15.

Eso no es una razón para no comprimir nunca. Es una razón para saber en qué caso estás:

  • Un enlace que lleva tu propio tráfico entre dos máquinas tuyas — replicación, copias de seguridad, registros, telemetría — no lleva texto plano elegido por un atacante viajando junto a secretos. Comprímelo, y comprime el flujo.
  • Un enlace que lleva navegación arbitraria de usuarios, donde el contenido web de otro y tus credenciales van juntos, es el caso sobre el que se escribió VORACLE. Déjalo apagado.

La razón de que me quede tranquilo poniendo esto en la construcción con tap y no en la de PPP no es un principio: aquí eliges un algoritmo moderno a propósito, para un tráfico que has mirado, mientras que el deflate de pppd comprime todo por defecto con uno de 1996 venga o no venga a cuento.

Qué Gana La Construcción Con Tap, Y Qué Cede

Pon las dos mitades una al lado de la otra, porque no compiten. Son compromisos distintos.

pppd sobre un sockettap o tun sobre un socket
Tramadoincorporado (HDLC, resincroniza tras el daño)ninguno sobre UDP porque no hace falta; inventarlo sobre TCP
Configuración de direccionesnegociada por IPCP e IPV6CPa mano en los dos extremos
Comprobación de vidaeco LCP, incorporadaninguna; la añades tú o el enlace muere en silencio
AutenticaciónPAP o CHAP disponibles, las dos débilesninguna en absoluto
Capasolo 33 con tun, 2 con tap
Puenteadonosí, con tap
Sobrecargabandera, cabecera y FCS por trama, más escapesnada, o 14 bytes con tap
Compresióndeflate y BSD, activadas por defecto, de 1996ninguna, o zstd por trama que añades y controlas
Root necesariosí, en todo momentopara crear el dispositivo; no para usarlo
Piezas en movimientoun demonio de treinta añosun descriptor de fichero

PPP te da un enlace negociado que se vigila solo y te cobra un protocolo por ello. Un dispositivo tap te da un agujero en crudo por nada, y tú pones las piezas que faltan o te las ahorras. Para un túnel que se queda en marcha, la comprobación de vida ausente es la que muerde: pppd se entera de un portador muerto en treinta segundos y lo reconstruye, la construcción con tap no se entera de nada porque no hay nada dentro que esté mirando. Añade un keepalive, ejecútalo bajo algo que lo reinicie, o usa la construcción que ya tiene uno.

Cuándo Es Esta La Herramienta Correcta, Y Cuándo No

Nunca es de verdad la herramienta correcta, y no voy a fingir lo contrario. Todo lo de arriba funciona, y nada de ello es lo que deberías ejecutar en producción. La versión honesta de un cómo-se-hace incluye la parte donde sueltas la herramienta, y esta es esa parte.

Para lo que sirve de verdad es para enseñarte cómo funciona una cosa. Una VPN desmontada en sus piezas y, en la sección siguiente, cómo se comporta de verdad la salida en cuanto alguien está dentro de tu red con root. Esas son las razones para haber leído esto. Los casos estrechos de abajo son reales, pero no son por lo que existe este artículo.

Echa mano de la construcción PPP cuando:

  • El portador no es IP en absoluto — una consola serie, un cacharro USB, un enlace de radio, una tubería con nombre, un canal SSH. A pppd le da igual sobre qué viajen los bytes, y un dispositivo tap no te ayuda aquí.
  • Estás rescatando algo: una máquina con consola serie, sin red, y un trabajo que hay que terminar esta noche. PPP sobre esa consola es un enlace enrutado, instalado en los dos extremos sin nada que copiar.
  • Quieres que el enlace se cuide solo. El eco LCP, la negociación de direcciones y el reinicio salen gratis; escribirlos tú es como la construcción con tap se convierte en un producto pequeño y poco fiable.

Echa mano de la construcción con tap cuando:

  • Necesitas capa 2 — un protocolo que no es IP, un latido de clúster que quiere difusiones, DHCP a través del túnel, o dos segmentos que tienen que ser uno.
  • Quieres las mínimas piezas en movimiento: sobre UDP con DTLS son dos comandos, sin demonio, sin negociación, sin nada que escapar.
  • Root escasea. Crea el dispositivo una vez con user, y el proceso que mueve tramas no vuelve a necesitar privilegios.

Las dos son la herramienta correcta cuando estás aprendiendo. Cada capa es visible y se puede cambiar por separado, y no hay mejor forma de entender qué hace un producto VPN que construir uno con las piezas y ver cómo se levanta cada una.

Ninguna es la herramienta correcta cuando quieres una VPN. Para eso, usa WireGuard. Está en el núcleo, es una fracción del código, hace la criptografía como es debido sin nada que negociar y nada que equivocar, y es un protocolo de datagramas por diseño. ssh -w te da un dispositivo tun sobre una sesión SSH existente en un solo comando, y OpenVPN es la versión madura y auditada de la forma DTLS-sobre-tap de arriba. Los tres son mejores en esto que cualquier cosa construida aquí.

Construye esto porque quieres saber qué hay dentro de la cosa que compras. No porque fuera ingenioso.

Lo Que Esto Enseña De Verdad: La Salida Desde El Lado Del Atacante

Esta es la razón para leer un artículo de construcción que te acaban de decir que no uses. Dale la vuelta y mira desde dentro de tu propia red, como quien acaba de aterrizar ahí con root. Toda brecha real acaba ahí, por una clave robada, una fuga de contenedor, un servicio sin parchear, alguien de dentro. La pregunta que decide entonces lo mala que se pone la jornada no es «qué pueden ejecutar», porque pueden ejecutar cualquier cosa. Es «qué puede salir, y si lo habías decidido antes de que llegaran».

Si la salida está abierta por defecto, la respuesta es todo, y ya poco puedes hacer. Nada de esto era exótico. pppd, ncat, socat e ip son paquetes firmados de la distribución que ya están en la máquina; el apaño de tap son cincuenta líneas de biblioteca estándar. Un puerto saliente permitido — y es el 443, el que abre toda red por defecto — y hay un enlace enrutado desde tu red a la de otro, cifrado, autenticado, que sobrevive a los reinicios, que lleva IPv4 e IPv6, e indistinguible en tu frontera de cualquier sesión HTTPS que tus usuarios hacen diez mil veces al día. Hazlo tap y puentéalo y lo que salió del edificio no es una ruta. Es el segmento.

No hay nada que pueda pillar un escáner: ni firma de malware porque no hay malware, ni protocolo raro porque es un saludo TLS normal al 443, ni binario inusual porque los instaló todos tu propio gestor de paquetes. El proxy registra una conexión y un recuento de bytes, y las dos cosas parecen trabajo.

Y el destino ni siquiera es fijo. Un socket no tiene por qué terminar donde acaban los paquetes, porque el portador se puede apuntar a través de un proxy — un relé que no es más que dos conexiones y una tubería, que la sección siguiente construye en una línea de shell. ncat acepta --proxy con --proxy-type http, socks4 o socks5, así que la sesión TLS que ve tu frontera termina en aquello a lo que el atacante le dijo que conectara a través — un salto interno, un servicio SaaS permitido que resulta reenviar CONNECT, un relé en la nube — y el túnel sigue desde ahí hasta algún sitio que tú no ves nunca. HTTP CONNECT y SOCKS hacen esto los dos por diseño, porque para eso está un proxy. Así que una entrada en la lista de permitidos para un destino en el que confías es siempre confianza en ese destino y en todo aquello a lo que vaya a reenviar, que ni controlas ni puedes enumerar. El punto final de tu registro de cortafuegos es el proxy. Nunca fue el extremo lejano.

Así que la verdad incómoda: en cuanto alguien está dentro con root y la salida es permisiva, el túnel no es lo que te toca impedir. Las piezas están instaladas, la salida está abierta, y ni siquiera lleva a donde parece. Tu única oportunidad de poner esto difícil fue antes de que llegara el atacante, en la frontera, decidiendo qué puede salir.

Eso es la salida con default-deny, y es la lección entera. Salida bloqueada por defecto; una lista de permitidos corta y nombrada donde una persona justificó cada destino y cada puerto; todo lo demás rechazado, registrado y con alerta. No porque frene en seco a un atacante decidido — un destino permitido es un túnel permitido — sino porque la alternativa es no tener decisión ninguna que hacer cumplir. Una política de salida escrita como una lista de puertos permitidos es una política sobre números de puerto. Nunca fue una política sobre qué sale, y en cuanto alguien tiene root, los números de puerto son todo lo que protege.

Este es el mismo hallazgo que el artículo sobre ping, que construye el túnel con eco ICMP, y el artículo sobre los ayudantes de protocolo, donde tu cortafuegos abre los agujeros él solo. Tres formas de entrar, una conclusión: el control que creías tener era sobre protocolos, y ninguno de estos protocolos es lo que dice ser. La frontera, decidida por adelantado y con default-deny, es el único control que fue real alguna vez.

Un Proxy Son Dos Conexiones Y Una Tubería

Merece la pena ver lo poco que es un relé, porque explica por qué no puedes razonar sobre el extremo lejano desde el cercano. Un proxy no es software especial. Es una conexión unida a otra por una tubería. La forma más vieja usa una tubería con nombre, un FIFO, para llevar el sentido de vuelta: un ncat escucha, otro conecta hacia delante, y el FIFO cablea el camino de respuesta entre ellos.

mkfifo backpipe
ncat -l 7000 0<backpipe | ncat farend.example.net 7100 1>backpipe

Léelo como fontanería: la salida de quien escucha entra en el segundo ncat y sigue hacia el extremo lejano, y las respuestas vuelven por el FIFO hasta el cliente. Dos sockets, una tubería, los dos sentidos, y la conexión del cliente termina aquí, en el relé, mientras los paquetes siguen hasta farend y vuelven. Ejecuté exactamente esto en loopback con un tercer ncat haciendo de eco en el extremo lejano, y una línea que metí volvió tras haber hecho el viaje entero.

Un relé son dos sockets unidos por una tubería, y el punto final se mueveUna conexión entra, otra sale, una tubería en medio. Eso es un proxy.clienteabre una sesión TLSRELÉ — el destino que registra tu fronterancat -l 7000termina al clientencat farendorigina un salto nuevoextremo lejanoel otro lado realstdoutbackpipe (FIFO) trae de vuelta las respuestascliente → relérelé → extremo lejanoLa conexión del cliente acaba en el relé. Los paquetes no.Tu cortafuegos registró una conexión a esta máquina. A dónde reenvía se decide dentro de la máquina, y encadenar tres de estas dejael extremo lejano real a tres tuberías — el registro de cada salto muestra solo una conexión local y formal al siguiente, y nada más allá.
Un relé son dos sockets y una tubería. Quien escucha termina la conexión del cliente. Un segundo netcat origina una conexión nueva hacia delante, y el FIFO lleva el sentido de vuelta entre ellos. La sesión TLS del cliente acaba aquí, en el relé, y empieza un salto nuevo — así que el destino que registró tu frontera es esta máquina, y los paquetes siguen hasta donde sea que ella reenvíe. Encadena tres y el extremo lejano está a tres tuberías, y el cortafuegos de cada salto solo ve una conexión local y ordenada hacia el siguiente.

Ncat hace lo mismo en un solo proceso, ejecutando la conexión hacia delante por cada cliente que llega:

ncat -l 7000 --keep-open --sh-exec 'ncat farend.example.net 7100'

La misma forma, menos piezas: el socket de quien escucha se une al del ncat ejecutado por la tubería que el shell pone entre ellos. Encadena tres y el túnel cruza tres redes, terminando y volviendo a originarse en cada una, y el cortafuegos de cada salto registra una conexión local y ordenada hacia el siguiente y nada más allá.

Ese es el truco entero, y por eso el registro de la frontera no es prueba de un destino. Cada relé es el extremo lejano hasta donde puede ver la máquina anterior, y el otro extremo de verdad está a tantas tuberías como nadie estuviera mirando.

Pon ahora esos relés en máquinas que no son del atacante.

Los relés encadenados por equipos comprometidos lavan el punto finalCada salto es la máquina de otro, y cada dueño ve solo el medioatacantela única máquina que es suyarelé 1la red de otra empresarelé 2un VPS secuestradorelé 3un router domésticodestinoa donde ibave: 1←→2ve: 2←→3ve: 3←→dest.Ningún salto ve más allá de sus dos vecinos. Sin origen, sin destino, solo medio.El tráfico se lava a través de una ristra de sistemas de otra gente, cada uno ejecutando el mismo relé de dos sockets y una tubería bajo elcontrol del atacante. Por eso el saliente de un equipo comprometido lleva tantas veces a otra víctima, no al atacante — veinte años de C2.
Cada salto es un equipo comprometido — la máquina de otra empresa, un VPS secuestrado, un router doméstico — ejecutando el mismo relé de dos sockets y una tubería bajo el mando y control del atacante. Ningún salto ve más allá de sus dos vecinos: el dueño del relé 2 ve una conexión del relé 1 y otra al relé 3, y nada más. El tráfico se lava a través de una ristra de sistemas de otra gente, y por eso la salida de un equipo comprometido lleva tantas veces a otra víctima en vez de al atacante.

Cada dueño de esa cadena ve solo una conexión del salto anterior al salto siguiente: sin origen, sin destino, solo medio. Esto no es una idea novedosa que yo le esté entregando a nadie. Es como han funcionado las cadenas de pivote y las redes de mando y control desde hace veinte años, y por qué el tráfico saliente de un equipo comprometido lleva tantas veces a otra víctima en vez de al atacante. Tus registros te enseñan que hablaste con una máquina en un centro de datos de algún sitio. De quién era, y a dónde reenvía, no estuvo nunca en ellos.

El peso defensivo es una línea: no puedes atribuir ni confiar en un destino que no restringiste por adelantado. Para cuando el tráfico está saliendo, la dirección a la que sale no te dice casi nada, porque es un relé en la máquina de otro y el punto final real está lavado detrás.

El Enlace Nunca Fue El Producto

Lo que me llama la atención, después de desmontar esto, es lo poco que tiene de nuevo y lo mucho que se vende.

El RFC 1661 es de 1994. pppd lleva treinta años en todas las distribuciones de Linux, los módulos del núcleo son ocho ficheros en un directorio, y todo lo que hace que una VPN sea una VPN — una interfaz virtual, un enlace negociado, un portador cifrado, una ruta — son cuatro programas y un certificado. Nada de eso es difícil ni secreto. Documentaron cada byte y lo regalaron, y creció una industria entre tú y eso vendiendo las mismas cuatro piezas en una caja con una licencia por puesto y un contrato de soporte que caduca. Las piezas no mejoraron. Las envolvieron.

Eso no es un argumento para ejecutar esto en producción. Acabo de decirte que no lo hagas. Es un argumento para saber qué hay en la caja que compras, porque el día que el proveedor cambie las licencias, lo compren o cierre tu modelo, la diferencia entre un mal trimestre y un mal año es si alguien de tu casa sabe de qué estaba hecha la cosa.

Cógete una tarde y construye el túnel con las piezas. Mira negociar a LCP, rompe el enlace y míralo volver, quítale el certificado y mira qué se para. Después vuelve a leer la hoja de datos de tu proveedor de VPN, y mira cuánto reconoces.

Esa misma tarde compra la otra mitad. Si un enlace enrutado hacia fuera de tu red son cuatro programas instalados y un certificado, a quien acaba de conseguir root en una de tus máquinas no lo frena lo difícil que sea construir el túnel. No es difícil. Lo único que lo frena es lo que tú decidiste, antes de que llegara, que podía salir. Constrúyelo una vez y dejas de pensar en la salida como algo que hace cumplir un producto, y empiezas a pensarla como una decisión que tomaste o no tomaste.

No hay mucha magia en ello. La mayor parte es 1994 con una mano de pintura, y no hay nowt de malo en 1994. Funcionó, estaba documentado, y todavía se ejecuta.


  1. RFC 1661 — The Point-to-Point Protocol (PPP), 1994. Define el enlace, LCP y la familia de protocolos de control de red que se apoyan encima. ↩︎

  2. RFC 2637 — Point-to-Point Tunneling Protocol (PPTP), que lleva PPP dentro de GRE. ↩︎

  3. RFC 3931 — Layer Two Tunneling Protocol version 3, el descendiente en vía de estándar del protocolo que lleva PPP dentro de UDP. ↩︎

  4. pppd(8) — la página de manual del demonio PPP. Fuente de pty, notty, local, passive, noipdefault, proxyarp, record, receive-all, las opciones de eco LCP y el valor por defecto de asyncmap: «If no asyncmap option is given, the default is zero, so pppd will ask the peer not to escape any control characters.» Las citas de aquí se leyeron de man pppd en ppp 2.5.1. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  5. RFC 1662 — PPP in HDLC-like Framing. La secuencia de bandera, la regla de relleno de octetos y el Async-Control-Character-Map: «Each frame begins and ends with a Flag Sequence, which is the binary sequence 01111110 (hexadecimal 0x7e).» ↩︎ ↩︎

  6. Olaf Titz, Why TCP Over TCP Is A Bad Idea — la explicación estándar del apilado de retransmisiones, que abre con esta construcción exacta. La URL original ya no sirve el artículo; esta es una captura del Internet Archive. ↩︎

  7. RFC 5072 — IP Version 6 over PPP. IPV6CP y los identificadores de interfaz de 64 bits, independientes de IPCP. ↩︎

  8. RFC 1994 — PPP Challenge Handshake Authentication Protocol (CHAP). Demuestra la identidad del par; no cifra nada. ↩︎

  9. RFC 3079 — Deriving Keys for use with Microsoft Point-to-Point Encryption (MPPE). La construcción RC4 con clave del intercambio MS-CHAP, y la razón de que PPTP no sea una opción viva. ↩︎

  10. Ncat Users’ Guide — connecting through a proxy — --proxy y --proxy-type para HTTP CONNECT y SOCKS 4/5, de modo que el portador TLS termina en el proxy, no en el extremo lejano del túnel. ↩︎

  11. Ncat Users’ Guide — las opciones --ssl, --ssl-verify y --ssl-trustfile, y los modos de escucha y UDP. Versión probada aquí: Ncat 7.92. ↩︎

  12. Universal TUN/TAP device driver — la documentación del propio núcleo para /dev/net/tun, TUNSETIFF, y la diferencia entre tun y tap. ↩︎

  13. PEP 784 — Adding Zstandard to the standard library, por lo que compression.zstd no necesita paquete en Python 3.14. Medido aquí contra Python 3.14.7 y zstd 1.5.7. ↩︎

  14. RFC 7457 — Summarizing Known Attacks on TLS and DTLS, que cubre CRIME y la forma general de una fuga por longitud al comprimir antes de cifrar. ↩︎

  15. OpenVPN — the VORACLE attack — la misma fuga contra una VPN que comprime antes de cifrar, y la razón de que OpenVPN desaconseje ahora la compresión. ↩︎