SSH into a home laptop from a phone over Tailscale
A Windows laptop at home is made reachable from an iPhone anywhere, with no open router port and no rented server. The piece rebuilds each layer from below: why NAT makes the laptop unreachable, how Tailscale separates coordination from traffic, what a WireGuard tunnel carries, how hole punching opens two NATs, and what an SSH public-key login proves. It ends with the Windows-specific failures met on the way, the operating rules of an always-on machine and an estimate of its yearly energy cost.
El portátil de este texto es un ThinkPad T14s que vive en un escritorio de casa y es el único ordenador que tengo allí. Quería su terminal en el móvil desde cualquier sitio, con Claude Code funcionando dentro como si estuviera sentado delante, y no quería alquilar un servidor ni abrir un puerto en el router para conseguirlo. El montaje llevó una tarde, la del 20 de octubre de 2026, y la parte de red fue la rápida: casi todo el tiempo se fue en Windows, y el hallazgo más útil del día fue un proceso de Python que llevaba veintidós horas quemando un núcleo del procesador.
Lo que hay aquí es cada capa de esa conexión reconstruida desde abajo: por qué no se puede llegar a un portátil de casa, cómo dos dispositivos detrás de dos NAT distintos acaban hablando directamente, qué demuestra en realidad un login SSH, dónde se interpuso Windows y cuánta electricidad cuesta un portátil que nunca duerme. El material son los registros y capturas de esa tarde. Las direcciones públicas de figuras y registros se han sustituido por direcciones de los rangos reservados para documentación, las privadas se abrevian como 100.x, y lo que identifica la cuenta está tapado en negro; nombres, horas y versiones son los reales.
El estado final: Claude Code lanzado en el portátil desde el móvil, en el prompt de PowerShell de la sesión SSH. Las líneas de arriba cierran una respuesta anterior cuya herramienta de PowerShell había fallado, uno de los problemas de Windows que se describen más abajo; el identificador de sesión y el plan están tapados.
En uso26 · oct 20Read noteEl primer uso de verdad llegó esa misma semana. Sentado en los jardincitos frente al hospital con un pain au chocolat, estudiando chino, escribí los apuntes directamente en el inbox del mazo de chino, en el portátil de casa. Hasta ahora iban a un chat de WhatsApp conmigo mismo, de buffer, y había que pasarlos después. Saltarse ese paso es una gozada.
Por qué no se puede llegar a un portátil de casa
Empieza por lo que tiene que hacer el móvil. Para abrir una sesión SSH envía un paquete TCP a la dirección del portátil, al puerto 22. En la wifi de casa la dirección del portátil es 192.168.1.20, que fuera de casa no significa nada, porque miles de otras casas tienen también una máquina en 192.168.1.20. Solo el router tiene una dirección hacia la que el resto de internet sabe enrutar, aquí 203.0.113.45, y toda la casa la comparte.
El router hace que ese reparto funcione con traducción de direcciones. Cuando el portátil envía un paquete hacia fuera, el router sustituye la dirección y el puerto del portátil por su propia dirección pública y un puerto que elige él, y apunta la pareja en una tabla. Una respuesta dirigida a ese puerto público se traduce de vuelta y se entrega al portátil. Un paquete que llega sin una línea que le corresponda en la tabla no es de nadie, y el router lo descarta. Por eso toda conversación a través de un NAT tiene que empezar desde dentro, y un móvil que quiera empezar una con el portátil no tiene a qué llamar.
El móvil está en la misma situación, un nivel más abajo. Con 4G no recibe dirección pública propia: el operador tiene un segundo NAT (el NAT de operador, o CGNAT) y pone miles de móviles detrás de cada dirección pública. Así que hay dos dispositivos, cada uno detrás de su NAT, los dos libres de empezar conversaciones y ninguno capaz de recibirlas.
Los dos NAT entre el móvil y el portátil. La tabla del NAT de casa (a, a la derecha) es lo único que permite que una respuesta vuelva al portátil, y solo guarda los destinos a los que el portátil ya ha escrito.
Hay tres salidas clásicas, y cada una cuesta algo distinto.
Redirigir un puertoSe le dice al router que mande al portátil todo lo que llegue a un puerto público. Resuelve el lado de casa, y deja el servidor SSH delante de todo internet, donde los escáneres automáticos prueban contraseñas en el puerto 22 a todas horas.
Un servidor de relevoUna máquina alquilada con dirección pública, a la que los dos dispositivos llaman. No queda nada de casa expuesto, pero cada byte da un rodeo por un servidor que pagas y mantienes.
Una VPN en mallaLos dos dispositivos llaman a un coordinador que solo los presenta, y luego intentan alcanzarse directamente, con un relevo de reserva para cuando no pueden. Es lo que hace Tailscale, y el resto del texto lo desmonta.
Uno presenta, los dispositivos hablan
Tailscale divide el trabajo en dos, y la división va primero porque decide qué puede ver y qué no la empresa que hay detrás. El plano de control es un servidor de coordinación en internet. Cuando instalé el cliente en el portátil e inicié sesión con una cuenta de Google, el portátil generó un par de claves, se quedó la mitad privada y subió la mitad pública junto con las direcciones en las que se le podía encontrar. El móvil hizo lo mismo unos minutos después. Después el servidor le habló a cada dispositivo del otro: esta es su clave pública, estas son sus direcciones. Eso es todo lo que el servidor hace con ellos.
El plano de datos es el conjunto de túneles cifrados entre dispositivos, y el servidor no forma parte de él. Cuando tecleo en el móvil, las pulsaciones llegan al portátil dentro de un túnel WireGuard cuyas claves solo existen en esos dos dispositivos. El servidor sabe que el móvil se conectó a las 16:54 desde alguna dirección; no tiene manera de saber qué se tecleó.
Dos planos. Las líneas que salen del servidor de coordinación llevan claves públicas y direcciones por HTTPS; el túnel entre los dispositivos lleva el tráfico. El relevo de abajo solo entra cuando no se forma un camino directo.
La división también decide qué se rompe. Si el servidor de coordinación cayera en mitad de una sesión, la sesión seguiría, porque el túnel no necesita nada de él. Lo que se pararía son los cambios: un dispositivo nuevo no podría entrar, y uno que se moviera a otra red no podría anunciar su nueva dirección. Los clientes son de código abierto y el servidor no, pero su parte se puede sustituir por Headscale, una reimplementación independiente y abierta del servidor de coordinación1.
1Headscale implementa solo el servidor de control; los clientes oficiales se conectan a él sin cambios.
La vista del plano de control antes de que entrara el móvil: una máquina, la versión de su cliente y su sistema operativo. Los datos de la cuenta y la dirección están tapados.
Un túnel sin nada que mantener vivo
Un túnel es un paquete dentro de otro paquete. El portátil y el móvil reciben cada uno una interfaz de red extra con una dirección en 100.64.0.0/10, un rango reservado para los NAT de operador que nunca aparece en internet. Cuando el móvil envía algo a la dirección 100.x del portátil, el sistema operativo le pasa el paquete IP completo a WireGuard, que lo cifra y lo envía como contenido de un datagrama UDP normal dirigido a donde de verdad se puede alcanzar al portátil. El portátil lo desenvuelve, lo descifra y entrega el paquete interior a su propia pila de red como si hubiera llegado por un cable.
WireGuard identifica a cada extremo por su clave pública y nada más. Cada dispositivo tiene un par de claves Curve25519, y antes del primer paquete de datos los dos hacen un saludo de un solo viaje de ida y vuelta, un intercambio de claves en el que cada uno demuestra que tiene la clave privada de su clave pública y ambos derivan claves de sesión nuevas. Los datos viajan después cifrados y autenticados con ChaCha20-Poly1305, y las claves de sesión se renuevan cada dos minutos1. Un paquete que no se autentica no recibe respuesta de ningún tipo, así que para un escáner el puerto del túnel parece cerrado.
1Donenfeld, WireGuard: Next Generation Kernel Network Tunnel, NDSS 2017; el intervalo de renovación es la constante del protocolo de 120 segundos.
El diseño tiene una consecuencia que importa más adelante, en la factura de la luz. WireGuard no mantiene ninguna conexión que haya que sostener: un extremo es una clave y una última dirección conocida, y el saludo solo ocurre cuando hay algo que enviar. Tailscale añade un poco encima (paquetes pequeños para mantener vivas las entradas de los NAT mientras se usa un camino, y un enlace discreto con un relevo), pero un túnel en reposo no cuesta casi nada.
## Medido en este portátil Rango del tailnet100.64.0.0/10espacio de direcciones compartido, no enrutado en internet MTU interior1280 bytesla interfaz de Tailscale ## Del protocolo Sobrecarga60 bytespor paquete sobre IPv4: 20 IP, 8 UDP, 16 cabecera, 16 etiqueta Intercambio de clavesX25519Curve25519 CifradoChaCha20-Poly1305 Renovación120 s
Una pulsación en el cable. Desde la cabecera IP interior hasta el final del paquete SSH, los routers intermedios solo ven texto cifrado; el túnel añade 60 bytes a cada paquete y limita el interior a 1280.
Dentro del túnel, la sesión SSH va cifrada una segunda vez con sus propias claves. Parece un desperdicio y es deliberado, porque las dos capas fallan por separado: un error en la configuración de la VPN deja la sesión cifrada igualmente, y una configuración SSH débil sigue siendo invisible para cualquiera de fuera del tailnet.
Atravesar dos NAT
El móvil con 4G y el portátil de casa están detrás de NAT que descartan los paquetes no pedidos, y aun así el túnel entre ellos va directo, sin ningún servidor en medio. Funciona porque, cuando cada paquete llega, ya no es un paquete no pedido.
El procedimiento es el NAT traversal, y en la secuencia de abajo lleva seis mensajes. Primero cada dispositivo pregunta a un servidor público desde qué dirección parecen salir sus paquetes; los servidores de relevo de Tailscale responden a esa pregunta con STUN, y la respuesta es la mitad exterior de la entrada de NAT que la propia pregunta acaba de crear. Después los dos dispositivos se intercambian esas respuestas a través del servidor de coordinación. Por último, los dos envían pequeños pings a la dirección exterior del otro. El primer ping del móvil llega al router de casa y se descarta, porque el portátil nunca le ha escrito al móvil. Ese ping descartado ya ha hecho su trabajo: el NAT del operador guarda ahora una entrada para la dirección del portátil, así que cuando el ping del portátil llega un momento después, el NAT del operador lo deja pasar. A partir de ahí los dos NAT tienen la entrada que necesita el otro lado.
El NAT traversal en seis mensajes, listados a la derecha. El ping fallido del paso 4 es lo que hace que el paso 5 funcione.
El método depende de una propiedad de los NAT implicados: el puerto exterior aprendido en el primer paso tiene que ser el mismo que el NAT usa cuando el dispositivo escribe a su pareja. Los NAT que mantienen el mismo puerto sea cual sea el destino (asignación independiente del destino) lo permiten; los que eligen un puerto nuevo para cada destino (a menudo llamados simétricos) lo impiden, porque la dirección que aprendió un lado no es la que el otro tendría que alcanzar1. Tailscale comprueba su propio lado con tailscale netcheck, y en el portátil dio esto:
1RFC 4787, NAT Behavioral Requirements for Unicast UDP, 2007, clasifica los comportamientos de asignación; Ford, Srisuresh y Kegel describen la técnica en Peer-to-Peer Communication Across Network Address Translators, USENIX ATC 2005.
TEXT Copy
* UDP: true * MappingVariesByDestIP: false * Nearest DERP: Madrid - mad: 21.4ms (Madrid)
MappingVariesByDestIP: false es la línea que importa: el router de casa mantiene un puerto exterior por cada puerto interior, así que la dirección que aprende el móvil es la que tiene que alcanzar. Con el móvil en la wifi de casa la cuestión ni siquiera se planteó. tailscale status mostraba el camino como active; direct 192.168.1.x:41641, porque Tailscale también ofrece las direcciones locales de cada dispositivo como candidatas, vio que los dos estaban en la misma red y mantuvo el tráfico dentro de casa. Cuando no se forma un camino directo, los paquetes pasan por un relevo DERP1. Habla HTTPS por el puerto 443, que atraviesa casi cualquier red, y reenvía paquetes WireGuard que no puede descifrar. El relevo cuesta un rodeo y sus límites de ancho de banda; el más cercano aquí está en Madrid, a 21 ms. No cuesta nada en privacidad, porque los paquetes que lleva son el mismo texto cifrado que llevaría un camino directo.
1Tailscale, DERP servers; una explicación más larga de todo el procedimiento es How NAT traversal works, de Tailscale.
La vista del tailnet desde el móvil: los dos dispositivos en línea. La cuenta y las direcciones están tapadas.El portátil visto desde el móvil: un nombre MagicDNS, una dirección IPv4 y otra IPv6 dentro del tailnet, y una clave que caducaría en cinco meses.
Qué demuestra un login SSH
Con el túnel levantado, el móvil puede llegar al puerto 22 del portátil, y entra en juego SSH. Un login SSH contiene dos comprobaciones separadas en un orden fijo, y protegen contra cosas distintas.
En la primera, el móvil comprueba el portátil. Los dos lados hacen un intercambio de claves que les da un secreto compartido y un identificador de sesión, y el portátil firma ese identificador con su clave de host. El móvil compara la clave de host con la que guardó en la primera conexión: ese era el aviso con la huella que Termius enseñó la primera vez y que acepté. Una clave distinta en una conexión posterior significaría que está contestando otra máquina. En la segunda, el portátil comprueba el móvil, por contraseña o por clave pública.
Un login SSH contiene dos comprobaciones. En la primera el móvil verifica la clave de host del portátil; en la segunda el portátil verifica una firma hecha dentro del Secure Enclave del móvil.
Empecé con contraseña, y falló, por una razón que explica la sección de Windows. El registro del servidor guardó los intentos:
TEXT Copy
16:32:50 sshd: Failed password for yagom from 100.x port 53125 ssh2 16:32:51 sshd: Failed password for yagom from 100.x port 53125 ssh2 16:54:20 sshd: Accepted publickey for yagom from 100.x port 53148 ssh2: ECDSA SHA256:khtO…
La tercera línea es el login con clave. Termius creó la clave como SSH ID dentro del Secure Enclave del iPhone, un chip que genera claves privadas y firma con ellas pero que no tiene ninguna operación para leer una clave hacia fuera. Ese chip solo maneja claves NIST P-2561, y por eso el registro dice ECDSA donde una clave generada por software sería normalmente Ed25519. Termius publicó además la mitad pública en sshid.io, lo cual no tiene riesgo, porque una clave pública está hecha para ser pública. Instalarla en el portátil fue una línea en un archivo, y ese archivo no era el evidente (otra vez la sección de Windows).
1Apple, SecureEnclave, documentación de CryptoKit: el enclave solo ofrece firma y acuerdo de claves P-256.
Con clave, el login es una firma. El móvil firma unos datos que incluyen el identificador de sesión, el portátil verifica la firma con la clave pública que tiene guardada, y nada secreto cruza la red. Como el identificador de sesión es nuevo en cada conexión, una firma grabada no sirve para ningún otro login1. Una contraseña, en cambio, viaja hasta el servidor y se puede adivinar, así que en cuanto la clave funcionó apagué del todo el login por contraseña, con PasswordAuthentication no en C:\ProgramData\ssh\sshd_config. Ahora la única entrada es la clave del móvil.
1RFC 4252, The Secure Shell (SSH) Authentication Protocol, sección 7: los datos firmados empiezan por el identificador de sesión.
Termius vuelve a pedir contraseña siempre que no tiene una clave que ofrecer. El aviso apareció antes de que existiera la clave, y otra vez después, hasta que el host se asoció a la clave.
Dónde se interpuso Windows
Nada de la parte de red hubo que arreglarlo. Windows sí, en seis sitios, y cada fallo fue silencioso: una barra de progreso quieta, un login rechazado sin motivo, un archivo que el servidor ignoraba sin decir nada. Dentro del portátil, un login cruza siempre las mismas estaciones en el mismo orden: el adaptador wifi recibe el datagrama UDP, Tailscale lo descifra y lo convierte en una conexión TCP al puerto 22, el firewall decide si esa conexión puede pasar, sshd la acepta, la comprobación de claves decide quién entra y arranca una shell. Los cinco primeros problemas de abajo están en ese camino y en ese orden; el sexto tiene que ver con la energía.
El servidor SSH no viene instalado. OpenSSH Server es un componente opcional de Windows, y Add-WindowsCapability pasa la petición al sistema de mantenimiento, que reevalúa todo el almacén de componentes antes de añadir nada. El registro de mantenimiento se llenó de líneas sobre paquetes WinPE que la máquina no tiene, y la barra de progreso estuvo quieta casi todos los veintidós minutos (de 15:17 a 15:39) que tardó en terminar. La regla de firewall que crea es demasiado amplia. La regla que instala OpenSSH admite el puerto 22 desde cualquier dirección en las redes que Windows considera privadas, la wifi de casa incluida. La desactivé y añadí otra que solo admite el puerto 22 desde 100.64.0.0/10, así que el servidor SSH responde al tailnet y a nada más. La shell por defecto tiene que ser una que un servicio pueda lanzar. sshd se ejecuta como SYSTEM y arranca lo que indique DefaultShell1. El PowerShell 7 de este portátil vino de la Microsoft Store, donde el ejecutable es un alias por usuario que activa una app empaquetada, y desde una sesión de servicio falla. Puse Windows PowerShell 5.1 en su lugar. Claude Code, usado por SSH, se topó con el mismo muro por su cuenta: su herramienta de PowerShell falló con The system cannot execute the specified program, como se ve en la primera captura, y pasó a Bash.
1Microsoft, OpenSSH Server configuration for Windows.
Los administradores guardan sus claves en otro sitio. Para una cuenta del grupo Administradores, sshd ignora el .ssh\authorized_keys del usuario y lee C:\ProgramData\ssh\administrators_authorized_keys, y también ignora ese archivo salvo que solo SYSTEM y Administradores puedan acceder a él1. La línea que sugiere sshid.io añade la clave al primer archivo y no habría cambiado nada.
1Microsoft, Key-based authentication in OpenSSH for Windows.
Windows Hello escondió la contraseña. La cuenta es una cuenta Microsoft, así que su contraseña de SSH es la de la cuenta Microsoft. La máquina tenía activada la opción solo permitir el inicio de sesión con Windows Hello para cuentas Microsoft, que quita la contraseña de la pantalla de bloqueo, y los logins SSH por contraseña fallaban incluso con la contraseña correcta. Desactivar la opción era una salida; pasar a claves, que mantiene la máquina sin contraseña y es mejor de todos modos, era la otra, y la que elegí. La máquina tiene que seguir despierta. Con el cargador conectado, la suspensión y la hibernación quedaron en nunca, la tapa ya estaba configurada para no hacer nada al cerrarse, y la pantalla ahora se apaga a los cinco minutos. Un portátil suspendido apenas consume y no responde a nada.
Todo el lado del servidor cabe en un script con permisos de administrador, que es el que ejecuté (la clave la instala otro más corto):
POWERSHELL Copy
# OpenSSH Server, reachable only through Tailscale Add-WindowsCapability -Online -Name (Get-WindowsCapability -Online -Name 'OpenSSH.Server*').Name Set-Service sshd -StartupType Automatic; Start-Service sshd
# Shell for SSH sessions: Windows PowerShell 5.1 New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell ` -Value "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
# Firewall: port 22 from the tailnet only Disable-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' New-NetFirewallRule -Name 'SSH-Tailscale-Only' -DisplayName 'SSH (Tailscale only)' ` -Direction Inbound -Protocol TCP -LocalPort 22 -RemoteAddress 100.64.0.0/10 -Action Allow
# Never sleep on mains power powercfg /change standby-timeout-ac 0 powercfg /change hibernate-timeout-ac 0
Mira el registro del servidor antes que el error del cliente.
Cada fallo de esta sección apareció en Get-WinEvent -LogName 'OpenSSH/Operational' con una línea clara (contraseña fallida, clave aceptada, conexión abortada) mientras el móvil solo decía que no podía iniciar sesión.
Lo que cuesta un portátil que nunca duerme
La cuenta es corta. Un consumo constante de un vatio durante un año son 8,76 kWh, que a un precio supuesto de 0,15 € por kWh son unos 1,30 € al año. Así que la pregunta es cuántos vatios consume el portátil mientras espera, y la respuesta honesta es que depende casi por completo de lo que se le deje haciendo (consumo en reposo).
Cuando lo miré, estaba haciendo mucho. La pantalla estaba configurada para no apagarse nunca, ni con cargador ni con batería. Tres servidores de desarrollo llevaban en marcha desde el 14 y el 18 de octubre, dos de ellos de proyectos en los que no estaba trabajando. Y un script de Python, lanzado por otra sesión de Claude Code la tarde anterior a las 17:37, llevaba veintidós horas atascado en un bucle, ocupando un núcleo entero del procesador. La carga total rondaba el 50% sin nadie al teclado. Cerrar los servidores olvidados y el script atascado la bajó a un 30%, y lo que quedaba era una compilación de este mismo sitio en otra sesión.
Consumo estimado en tres estados y su coste anual. El montaje en sí (Tailscale y el servidor SSH esperando paquetes) añade demasiado poco para dibujarse a esta escala; el estado de la máquina decide la factura.
Son estimaciones a partir de consumos típicos de un portátil de esta clase, no mediciones. El portátil se puede medir a sí mismo sin vatímetro: desenchufado, con la pantalla apagada, durante una hora, su consumo medio es la capacidad de la batería por la fracción que ha perdido, así que una batería de 58 Wh que baja un 9% en una hora está consumiendo unos 5,2 W. Un portátil que está siempre enchufado tiene además un problema de batería. Una batería de iones de litio envejece más deprisa cuando se mantiene llena y caliente, que es justo el estado de un portátil usado como servidor1. Lenovo Vantage tiene un umbral de carga para esto, y lo activé con sus valores por defecto: empieza a cargar por debajo del 75% y se detiene en el 80%. La batería indicaba el 98% de su capacidad de diseño tras 32 ciclos, así que parte sana.
1Battery University, BU-808: How to Prolong Lithium-based Batteries, sobre el efecto del estado de carga y la temperatura en la pérdida de capacidad.
La batería antes del cambio: 98% de la capacidad de diseño tras 32 ciclos, con un cargador de 90 W.El umbral de carga, capturado justo antes de activarlo; el rango se dejó en sus valores por defecto. El número de serie de la batería está tapado.
El día a día
La mayoría de los sucesos no me piden nada, porque Tailscale y el servidor SSH arrancan como servicios de Windows antes de que nadie inicie sesión. Los que sí piden algo son los que una sola máquina no puede resolver por su cuenta.
Suceso Qué pasa Qué hacer
Apagado Inalcanzable; la consola lo muestra desconectado. Nada en casa puede encenderlo a distancia Pulsar el botón al volver a casa
Reinicio o actualización Los dos servicios arrancan antes del inicio de sesión; vuelve a estar accesible en un minuto o dos Nada
Suspensión Inalcanzable. Desactivada con cargador Mantenerlo enchufado
Corte de luz Sigue con batería, pero el router también se apaga; con batería Windows puede suspenderlo Nada hasta que vuelva la luz
Tapa cerrada Con cargador, no pasa nada Cerrarla si se quiere
Caducidad de la clave Las claves de Tailscale caducan a los 180 días; desactivado para el portátil Mantener la caducidad en el móvil
Móvil perdido La clave no puede salir del Secure Enclave, pero un móvil desbloqueado aún puede firmar Quitar el móvil en la consola y borrar su línea del archivo de claves
La caducidad de 180 días es la opción por defecto de Tailscale1. En el portátil supondría una máquina inalcanzable dos veces al año hasta que alguien volviera a iniciar sesión delante de ella, así que ahí la desactivé; el móvil la mantiene, porque la clave de un móvil perdido debe caducar sola. Renombrar un dispositivo en la consola de Tailscale cambia su nombre MagicDNS. Renombré el portátil de su nombre de Windows a thinkpad-64, y el nombre antiguo dejó de resolverse desde el móvil en el acto; el nombre de Windows no cambió.
1Tailscale, Key expiry.
Lo siguiente que añadiría
Lo primero, sesiones que sobrevivan a que el móvil pierda cobertura: hoy una conexión caída termina la shell y la sesión de Claude Code que corre dentro, y en Windows la respuesta habitual (un multiplexor de terminal como tmux) solo existe dentro de WSL. Y la medición de consumo de más arriba, para que la fila más barata del gráfico pase de ser un intervalo a ser un número.