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:

  1. 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_ID quedó vacío (visible en el output: device ID: (vm-playground), sin ID). El registro de ese ID vacío en vm-personal devolvió HTTP 200 igual (Syncthing no valida el deviceID contra un formato/checksum en ese POST), así que no se detectó hasta revisar el listado de devices de vm-personal y notar que vm-playground no aparecía. Fix aplicado al script: install_syncthing ahora, después de confirmar que existe config.xml, sondea /rest/system/status hasta recibir un myID no 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_info y 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.
  2. syncthing-add-folder.sh reemplazaba el arreglo devices del folder en vez de agregarle uno: el POST /rest/config/folders construía devices solo 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 .ssh en vm-personal quedó con solo 2 devices, perdiendo a surface-ehas, minibook-pop, t640-pop y termux (los otros 4 que ya compartían esa carpeta). Sin error ni warning — se detectó comparando el devices del folder antes/después del script. Fix aplicado al script: antes de cada POST hace GET /rest/config/folders/<id> en ese host y une (unique) su devices actual con los nuevos, preservando también el label existente 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.

  1. API key de cada instancia: <apikey> en ~/.local/state/syncthing/config.xml (o ~/.config/syncthing/config.xml según la instalación) — no hay que generarla, ya existe desde la primera vez que arrancó el daemon.
  2. POST /rest/config/folders en el lado origen, con ambos deviceID (el propio y el del destino) en el arreglo devices.
  3. El mismo POST /rest/config/folders en el lado destino, mismo id de carpeta, mismo par de deviceID. Si la ruta no existe todavía ahí, crearla a mano primero (mkdir -p) — Syncthing no crea el directorio padre completo por sí solo.
  4. No hace falta reiniciar el servicio ni aprobar nada por GUI — el POST en ambos lados basta porque el pairing de dispositivo (el paso que sí requiere confirmación manual) ya existía de antes. Verificar con GET /rest/db/status?folder=<id>: needFiles: 0 y state: idle en ambos lados confirma que ya sincronizó.
  5. Para cambiar el type de 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 el POST inicial.

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ía apt, habilita systemctl --user y activa loginctl 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 el POST haya devuelto 200.
  • syncthing-add-folder.sh <folder-id> <type> <host:ruta> [<host:ruta> ...] — comparte una carpeta entre hosts ya emparejados (precondición: correr syncthing-add-host.sh primero si algún par es nuevo). type es uno solo para todos los hosts en esta versión (sendreceive/sendonly/receiveonly); para tipos mixtos por host, ajustar después a mano con PATCH /rest/config/folders/<id>. Termina esperando state: idle y needTotalItems: 0 en cada host, no solo el POST inicial.
  • 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 a PUT /rest/config si esa versión de Syncthing no soporta el verbo DELETE ahí). Nunca toca /rest/config/devices ni 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/config que 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