Syncthing
Syncthing sincroniza config personal (hosts SSH, llaves) entre las computadoras de escritorio de Esteban y vm-personal, que actúa como punto central — el mismo rol que cumple para acceso SSH entre VMs. Se instala vía apt, corre como servicio de usuario (systemctl --user enable --now syncthing.service), y el pairing entre pares se aprueba a mano desde la GUI web de cada uno (localhost:8384) — no hay forma de aprobar un device ID nuevo sin esa confirmación manual en ambas puntas, es una medida de seguridad intencional del protocolo.
surface-wsl ↔ ctunlinux, carpeta proyectos (2026-09-03)
Primer pairing de Syncthing hacia Trivasa, y primera vez que se sincroniza una carpeta de
trabajo real (no .ssh ni claude-skills). A diferencia de esos dos casos, esta vez
no se sincroniza .ssh — es la carpeta de exploración/scratch de BI de Trivasa,
sendreceive en ambos lados: surface-wsl:/home/esteban/proyectos ↔
ctunlinux:/home/ealcocer/proyectos. Motivado por tener el mismo trabajo de exploración
SQL/Python disponible y editable desde ambas máquinas sin copiar a mano; el contenido de
ambos lados ya se había reconciliado antes de compartir (dos subcarpetas —
conciliacion-master y notificacion-solicitud-material — existían en ambos hosts con
avances independientes; se comparó archivo por archivo y se adoptó la versión más avanzada
de cada una antes del primer sync, para no depender de que Syncthing resolviera conflictos
de contenido real por su cuenta).
Bloqueo previo al pairing, no relacionado con Syncthing: syncthing-add-host.sh instala
vía apt, y el apt-get update de ctunlinux fallaba completo por una llave GPG rotada de
un repo ajeno (mssql-release.list, Microsoft SQL Server tools) — un solo repo sin firmar
tira todo el update, aunque Syncthing no tenga nada que ver con SQL Server. Se optó por
arreglar la llave de una vez (en vez de deshabilitar el repo solo durante la instalación):
la llave “genérica” de Microsoft (packages.microsoft.com/keys/microsoft.asc) no trae la
que este repo específico usa — Microsoft rotó a una llave más nueva
(EE4D7792F748182B, “Microsoft Corporation - General GPG Signer”) que solo se consiguió
vía gpg --recv-keys contra keyserver.ubuntu.com, exportada y agregada (append, no
reemplazo) al keyring que el repo ya esperaba en /usr/share/keyrings/microsoft-prod.gpg.
Detalle completo, incluyendo por qué no bastaba la key genérica, en
ctunlinux.
Hallazgo real durante el pairing — discovery/relay público no funciona desde ctunlinux:
tras instalar Syncthing ahí, syncthing-add-host.sh registró los device IDs cruzados sin
error (HTTP 200 en ambos lados) pero la verificación final de conexión activa nunca la vio
conectada. El log de Syncthing en ctunlinux mostraba el relay listener cayendo en loop con
tls: failed to verify certificate: x509: certificate signed by unknown authority contra
relays.syncthing.net — la salida HTTPS de ese host pasa por un Fortinet que hace
inspección SSL (certificados re-firmados con la CA del firewall, no confiada por el sistema)
, ver el hallazgo completo en ctunlinux. Esto
rompe tanto el discovery global como el relay público — cualquier folder nuevo entre
ctunlinux y un host fuera de su LAN que dependa de addresses: ["dynamic"] se va a quedar
sin conectar de la misma forma, no fue un problema puntual de este pairing.
Fix: en vez de instalar la CA del Fortinet en el trust store (decisión que no le
corresponde tomar a una sesión de Claude Code sin más contexto), se confirmó que
surface-wsl sí puede alcanzar el puerto 22000 de ctunlinux directamente — tanto por su
IP de LAN (192.168.117.14, la misma que ya usa el alias SSH TRV_ctunlinux) como por su
IP de NetBird (100.71.141.100) — y se fijaron ambas como direcciones estáticas del device
ctunlinux del lado de surface-wsl, vía PATCH /rest/config/devices/<id>:
{"addresses":["tcp://192.168.117.14:22000","tcp://100.71.141.100:22000","dynamic"]}
(se dejó dynamic al final como fallback, por si el discovery se arregla más adelante). No
hizo falta tocar nada del lado de ctunlinux — con que un solo lado sepa marcar directamente
al otro alcanza para establecer la conexión, y quien la inicia no importa para la
sincronización bidireccional posterior. Conectó en el segundo intento de polling
(tcp-client, dirección 192.168.117.14:22000).
Casi-incidente al compartir la carpeta: conciliacion-master/layout-gastos/ en
ctunlinux tenía un streamlit_venv/ completo (site-packages de Python 3.14, cientos de MB)
que nunca se había excluido porque hasta ese momento nadie más que ctunlinux tocaba esa
carpeta. Al correr syncthing-add-folder.sh sin .stignore previo, el sendreceive
bidireccional empezó a jalarlo hacia surface-wsl de inmediato (458MB ya copiados cuando se
detectó, con ~163MB todavía en cola) — se cortó agregando .stignore en ambos hosts vía
POST /rest/db/ignores?folder=proyectos (streamlit_venv, __pycache__, *.pyc, .venv,
venv, node_modules) y borrando a mano el streamlit_venv parcial que ya había llegado a
surface-wsl — Syncthing no revierte solo un pull en curso al agregar un ignore, hay que
limpiar el archivo/carpeta ya copiada aparte. Lección para la próxima carpeta nueva:
revisar contenido (du -sh */* ) antes de correr syncthing-add-folder.sh, no después —
syncthing-add-folder.sh en su versión actual no acepta patrones de ignore como argumento,
así que hoy por hoy siempre es un paso manual posterior al primer POST, con la ventana de
riesgo de que ya haya empezado a transferir algo que no debía.
vm-playground se sumó el 2026-08-26
Hasta esta fecha vm-playground no tenía Syncthing instalado ni participaba de la
malla — el folder .ssh solo corría entre las computadoras de escritorio y vm-personal,
nunca en las VMs de Oracle. Consecuencia práctica encontrada ese día: el
~/.ssh/config de vm-playground era un archivo mínimo armado a mano, sin los alias
TRV_/CK_/GCP_/OCI_/WS_ de SSH config de la
flota ni las llaves privadas correspondientes — sin acceso SSH a
ctunlinux ni al resto de Conkafecito/Trivasa desde ese host.
Se agregó con los scripts de scripts/syncthing/ de este repo:
syncthing-add-host.sh local vm-personal (empareja) y luego
syncthing-add-folder.sh 3quma-d9dqj sendreceive local:/home/ubuntu/.ssh vm-personal:/home/esteban/.ssh (comparte el folder .ssh ya existente). Antes de
compartir se hizo un backup manual de ~/.ssh (cp -a ~/.ssh ~/.ssh.pre-syncthing-backup-<fecha>)
por el mismo riesgo que ya documenta la sección de abajo — al ser sendreceive, el
config/known_hosts propios de vm-playground se reemplazaron por los de la flota
(el archivo viejo sobrevivió como config.sync-conflict-..., ningún dato se perdió).
Dos bugs reales encontrados al ejecutar syncthing-add-host.sh, arreglados a mano en
el momento y luego corregidos en los scripts mismos (scripts/syncthing/ de este repo)
el mismo día, con la corrección verificada re-ejecutando el comando exacto que había
disparado el segundo bug:
- Condición de carrera en
syncthing-add-host.sh: justo después de instalar Syncthing en el host nuevo, el script leía su device ID llamando a la API REST local sin esperar a que estuviera lista —NEW_IDquedó vacío (visible en el output:device ID: (vm-playground), sin ID). El registro de ese ID vacío en vm-personal devolvióHTTP 200igual (Syncthing no valida eldeviceIDcontra un formato/checksum en ese POST), así que no se detectó hasta revisar el listado de devices de vm-personal y notar quevm-playgroundno aparecía. Fix aplicado al script:install_syncthingahora, después de confirmar que existeconfig.xml, sondea/rest/system/statushasta recibir unmyIDno vacío antes de devolver el control — mismo patrón de espera activa que ya usaba para el archivo, aplicado también a la API.host_infoy el flujo principal además abortan explícitamente si algún device ID sale vacío, en vez de seguir con un registro silenciosamente roto. syncthing-add-folder.shreemplazaba el arreglodevicesdel folder en vez de agregarle uno: elPOST /rest/config/foldersconstruíadevicessolo con los hosts pasados como argumento (local+vm-personal, 2 en este caso) y lo mandaba tal cual — como Syncthing trata ese campo como el estado completo del folder, el folder.sshen vm-personal quedó con solo 2 devices, perdiendo asurface-ehas,minibook-pop,t640-popytermux(los otros 4 que ya compartían esa carpeta). Sin error ni warning — se detectó comparando eldevicesdel folder antes/después del script. Fix aplicado al script: antes de cadaPOSThaceGET /rest/config/folders/<id>en ese host y une (unique) sudevicesactual con los nuevos, preservando también ellabelexistente si lo había — ya no reemplaza a ciegas. Verificado re-ejecutando el mismo comando (3quma-d9dqj sendreceive local:... vm-personal:...) contra el folder real: quedó con los 6 devices correctos, no 2.
Puertos estándar: 22000 (TCP/UDP) para sincronización entre dispositivos, 8384 para la GUI web local de administración.
Agregar una carpeta por API en vez de la GUI
Entre dos dispositivos ya pareados (aprobación manual ya hecha una vez), agregar una carpeta nueva no requiere la GUI — se puede hacer con la REST API en ambas puntas, sin clicks. Caso real que motivó documentar esto: sincronizar ~/.claude/skills de pop-os hacia vm-personal, 2026-08-09 — reemplazado por un repo git ese mismo día (ver nota al final de esta sección); los pasos de abajo siguen vigentes como procedimiento general para cualquier otra carpeta.
- API key de cada instancia:
<apikey>en~/.local/state/syncthing/config.xml(o~/.config/syncthing/config.xmlsegún la instalación) — no hay que generarla, ya existe desde la primera vez que arrancó el daemon. POST /rest/config/foldersen el lado origen, con ambosdeviceID(el propio y el del destino) en el arreglodevices.- El mismo
POST /rest/config/foldersen el lado destino, mismoidde carpeta, mismo par dedeviceID. Si la ruta no existe todavía ahí, crearla a mano primero (mkdir -p) — Syncthing no crea el directorio padre completo por sí solo. - No hace falta reiniciar el servicio ni aprobar nada por GUI — el
POSTen ambos lados basta porque el pairing de dispositivo (el paso que sí requiere confirmación manual) ya existía de antes. Verificar conGET /rest/db/status?folder=<id>:needFiles: 0ystate: idleen ambos lados confirma que ya sincronizó. - Para cambiar el
typede una carpeta que ya existe (ej. de direccional a bidireccional),PATCH /rest/config/folders/<id>con{"type": "..."}en cada lado — no hace falta recrear la carpeta ni volver a pasar por elPOSTinicial.
claude-skills quedó en sendreceive en ambos lados (bidireccional) por decisión explícita de Esteban — a diferencia de .ssh, aquí no preocupa un *.sync-conflict-* ocasional porque no hay llaves privadas de por medio, así que se prefirió simplicidad sobre la protección extra de un sendonly/receiveonly direccional.
Actualización, mismo día (2026-08-09): Esteban cambió de opinión y decidió mover la distribución de ~/.claude/skills de Syncthing a un repo git dedicado — ver claude-skills-repo. El folder claude-skills se desmontó de Syncthing en los tres hosts (pop-os, vm-personal, vm-backup) con syncthing-remove-folder.sh (abajo), sin tocar los archivos en disco ni los device pairings — .ssh sigue sincronizando igual que antes entre pop-os y vm-personal. vm-backup queda emparejado (device) pero sin ningún folder compartido activo por su parte, salvo el default que Syncthing crea solo al instalarse y que nunca se usó.
Scripts reutilizables (scripts/syncthing/ de este repo)
Los pasos de arriba (pairing de dispositivo, compartir carpeta) quedaron como dos scripts bash independientes en scripts/syncthing/ de este mismo repo — pairing y carpeta son operaciones separadas en Syncthing, así que son dos scripts separados, no uno solo con flags. Viven en el repo (no solo en el disco de un host) para que sobrevivan a la reinstalación de cualquier máquina y cualquier sesión los pueda usar con solo clonar/hacer git pull; en pop-os están enlazados en ~/scripts/syncthing (symlink al checkout del repo) por costumbre de invocación, pero la fuente real es este directorio.
syncthing-add-host.sh <alias_nuevo> <alias_existente> [...]— empareja un host nuevo con uno o más existentes. Si el host nuevo no tiene Syncthing instalado, lo instala víaapt, habilitasystemctl --usery activaloginctl enable-linger(obligatorio en VMs sin sesión de escritorio — sin linger, el servicio de usuario muere al cerrar la sesión SSH). Termina verificando que la conexión quede activa (/rest/system/connections), no solo que elPOSThaya devuelto 200.syncthing-add-folder.sh <folder-id> <type> <host:ruta> [<host:ruta> ...]— comparte una carpeta entre hosts ya emparejados (precondición: corrersyncthing-add-host.shprimero si algún par es nuevo).typees uno solo para todos los hosts en esta versión (sendreceive/sendonly/receiveonly); para tipos mixtos por host, ajustar después a mano conPATCH /rest/config/folders/<id>. Termina esperandostate: idleyneedTotalItems: 0en cada host, no solo elPOSTinicial.syncthing-remove-folder.sh <folder-id> <host> [<host> ...]— lo opuesto: quita el share de un folder en uno o más hosts (DELETE /rest/config/folders/<id>, con fallback aPUT /rest/configsi esa versión de Syncthing no soporta el verboDELETEahí). Nunca toca/rest/config/devicesni el filesystem — solo el share. Verifica que el folder ya no aparezca y que el resto de folders/devices del host no haya cambiado.
Ambos usan alias de ~/.ssh/config tal cual (o local para la propia máquina) y son 100% vía REST API — nunca tocan la GUI de ningún host, así que sirven igual en una VM sin escritorio.
Caso real, 2026-08-09: se usaron para sumar vm-backup a la carpeta claude-skills (que ya sincronizaba entre pop-os y vm-personal). vm-backup no tenía Syncthing instalado — el primer script lo instaló, emparejó con pop-os y vm-personal, y el segundo compartió la carpeta en los tres. vm-backup corre como usuario ubuntu (no esteban), así que su ruta de la carpeta es /home/ubuntu/.claude/skills, distinta a la de los otros dos hosts — cada host:ruta en syncthing-add-folder.sh es independiente por eso mismo.
Consecuencia observada de sincronizar ~/.ssh/: al compartir esa carpeta entre minibook, pop-os y vm-personal, cualquier escritura simultánea en config o en una llave privada genera un *.sync-conflict-* (varios ya presentes en minibook, ej. config.sync-conflict-... y google_compute_engine.sync-conflict-...). No son solo ruido — implica que hay llaves privadas replicadas entre varios equipos, lo cual es un riesgo si alguno se pierde o se compromete: conviene revisar esos conflictos y evaluar si ~/.ssh/ debería seguir sincronizándose completo o solo un subconjunto (ej. vía .stignore).
Advertencia: perfiles de navegador no deben vivir en carpetas sincronizadas
Chromium/Brave usa una base de datos LevelDB con locking exclusivo por proceso (SingletonLock, que además guarda el hostname que lo creó). En el ~/.config/BraveSoftware/ de minibook-pop (recién reinstalado a Pop!_OS) apareció un SingletonLock apuntando a pop-os-9377 — el host viejo (t640-pop) — y bloqueaba a Brave aunque no hubiera ningún proceso corriendo ahí. No está confirmado si el origen fue un folder de Syncthing con alcance más amplio del que debería, o el propio directorio home preservado/restaurado durante la reinstalación del sistema; en cualquier caso, si el perfil de Brave llega a quedar dentro del alcance de un folder de Syncthing compartido entre estos hosts, hay que excluirlo explícitamente (.stignore) — sincronizar una base de datos de navegador en caliente puede corromper el perfil, no solo trabar el lock. Fix puntual mientras tanto: confirmar que no hay proceso Brave corriendo (ps aux | grep brave) y borrar SingletonLock, SingletonCookie y SingletonSocket a mano.
Véase también
- Acceso SSH entre VMs — mismo rol de vm-personal como hub, para el acceso administrativo en vez de la sincronización de archivos
- SSH config de la flota — el
~/.ssh/configque vm-playground ganó al sumarse a la malla el 2026-08-26 - NetBird — malla sobre la que corren estas conexiones cuando los hosts no están en la misma LAN
- ctunlinux — el host de Trivasa al que vm-playground ganó acceso SSH directo gracias a este cambio