Montar tu propio servidor
Ejecutar tu propio servidor es la razón de ser de este proyecto, no una nota al pie. No hay ningún servicio alojado al que apuntarse, ninguna cuenta con nosotros, y nada que llame a casa. Dos contenedores y un archivo de configuración.
Los repositorios
Todo lo que describe esta página está publicado bajo la licencia MIT.
| Repositorio | Qué es |
|---|---|
| flamenet-relay | El servidor que estás a punto de ejecutar |
| flamenet-e2e | El protocolo: motor en JavaScript, especificación y vectores de prueba publicados |
| flamenet-messenger-ios | La aplicación para iOS, y la fuente de verdad de todo lo anterior |
| flamenet-messenger-android | La aplicación para Android, con su propio motor en Kotlin |
Para ejecutar un servidor necesitas el primero. Los demás están ahí para que "se puede montar en casa" sea algo que puedas comprobar, y no algo que te cuentan.
Arranque rápido
git clone https://gitlab.com/paulhitt/flamenet-relay
cd flamenet-relay
cp .env.example .env
Genera los tres secretos que pide .env:
openssl rand -hex 32 # POSTGRES_PASSWORD
openssl genpkey -algorithm ed25519 -out issuer.pem
openssl pkey -in issuer.pem -outform DER | tail -c 32 | base64 # FN_ISSUER_PRIVATE_KEY
openssl pkey -in issuer.pem -pubout -outform DER | tail -c 32 | base64 # FN_ISSUER_PUBLIC_KEY
-hex para la contraseña de la base de datos, no
-base64, y esto no es cosmético. Se interpola en
postgres://flamenet:<contraseña>@db:5432/flamenet, donde
los caracteres /, + y = de base64
tienen todos significado. Una contraseña con una / hace esa URL
inválida y el servidor muere al arrancar con un escueto Code=-1000
y la cadena de conexión devuelta, que se lee como si la base de datos fuera
inalcanzable y no como si la contraseña necesitara escaparse.
openssl rand -base64 32 produce una de esas más o menos
tres de cada cuatro veces, así que seguir las instrucciones
antiguas solía fallar.
Pon en FN_CONTACT_EMAIL un buzón que leas, para que las
páginas de contacto y de seguridad personal te nombren a ti y no a nadie, y
arranca:
docker compose up -d
Cada variable de .env llega al servidor. El archivo compose
pasa el archivo entero, así que un ajuste que añadas ahí es un ajuste que el
servidor lee, y nada se descarta en silencio por no estar en una lista.
Eso descarga una imagen ya construida, así que no hay que instalar la cadena de herramientas de Swift ni compilar nada. Las imágenes son solo amd64: las construye la integración continua de este proyecto, que corre en una máquina x86_64. En una Raspberry Pi o en un VPS ARM, construye desde el código:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
Esa es una vía soportada, no un plan B, y también la que hay que usar si prefieres ejecutar lo que puedes leer antes que una imagen que construyó otro. docs/REPRODUCIBLE.md explica hasta dónde llega hoy esa comprobación, incluido dónde se detiene.
Abre http://localhost:8080. El servidor sirve su propia portada, así que una instalación que funciona se explica sola en vez de contestar con un 404.
Qué son esos secretos
FN_ISSUER_PRIVATE_KEY/FN_ISSUER_PUBLIC_KEY: un par de claves Ed25519. El servidor emite tokens de corta duración firmados con la mitad privada y los verifica con la pública, que es configuración y nunca se descarga de la red. Conservaissuer.pem: perderlo invalida todos los tokens que hayas emitido, y filtrarlo deja que cualquiera acuñe tokens que tu servidor acepta.POSTGRES_PASSWORD: la base de datos. El archivo compose no la publica en el anfitrión; el servidor llega a ella por la red de compose.
Almacenamiento
Postgres por defecto en el archivo compose. El servidor también funciona sobre SQLite, que es lo que usa un único binario sin segundo proceso:
FN_ISSUER_PUBLIC_KEY=... FN_ISSUER_PRIVATE_KEY=... \
flamenet-relay serve --hostname 0.0.0.0 --port 8080
Con FN_POSTGRES_URL definida usa Postgres; sin definir,
escribe relay.sqlite. El esquema es idéntico en los dos
casos.
Los adjuntos son archivos y no filas, así que viven en un volumen en ambos
modos (FN_ATTACHMENTS_DIR, /data/attachments dentro
del contenedor).
Ponerlo detrás de TLS
El archivo compose escucha en 127.0.0.1:8080 a propósito.
Termina el TLS delante con lo que ya tengas, Caddy, nginx, Traefik, y apúntalo
a ese puerto. Dos cosas importan:
- HTTPS no es opcional en la práctica. Los clientes llevan material de claves; servírselo por HTTP en claro se lo entrega a cualquiera en el camino.
- Reenvía la dirección del cliente. Los envíos sellados se
miden por destinatario y por IP, y detrás de un proxy que no pone
X-Forwarded-Forcada petición parece venir del proxy.
Retención
El servidor purga con un temporizador propio, dentro del proceso. No hay que programar nada externo, y no hay cron que se atasque. Los valores por defecto:
| Se borra a los | |
|---|---|
| Sobres entregados | 1 día |
| Sobres no entregados | 30 días |
| Adjuntos | 30 días |
| Claves previas de un solo uso ya usadas | 7 días |
Cambia cualquiera con FN_DELIVERED_RETENTION_DAYS,
FN_UNDELIVERED_RETENTION_DAYS y
FN_ATTACHMENT_RETENTION_DAYS.
Ya no existe un almacén de mensajes sin cifrar, así que
FN_MESSAGE_RETENTION_DAYS y
FN_READ_MESSAGE_RETENTION_DAYS no se leen. Definirlas no hace
nada; ver docs/PLAINTEXT.md en el repositorio del servidor.
Los archivos compose, y qué ejecuta cada uno
Cinco archivos, en capas. docker-compose.yml es la base y los
demás son capas encima, así que la forma es
-f docker-compose.yml -f <capa>.
| Archivo | Servicios | Para |
|---|---|---|
docker-compose.yml |
db, relay |
La base. Publica en 127.0.0.1; lo que usa el arranque
rápido. |
docker-compose.build.yml |
relay |
Construye la imagen desde el código en vez de descargarla. Necesario en ARM. |
docker-compose.prod.yml |
db, redis, relay,
coturn, obfs4bridge |
Un servidor público: límites de velocidad que sobreviven a un reinicio, TURN para las llamadas, y un puente para redes que bloquean el servidor. |
docker-compose.federated.yml |
db, redis, relay,
caddy, coturn |
Lo mismo, con Caddy terminando el TLS por ti. |
docker-compose.pooled.yml |
pgbouncer, relay |
Agrupación de conexiones, para cuando un Postgres sirve a varios
servidores. Una capa sobre prod, no sobre la base: usa la red
internal que define ese archivo. |
redis es opcional y el servidor lo dice al
arrancar. Sin él, los límites de velocidad se cuentan dentro del
proceso: correcto para un solo servidor, y se reinician con un reinicio. Con
él se comparten y perduran. Nada más depende de él.
obfs4bridge es para alcanzabilidad, no para
privacidad. Le da al servidor una segunda dirección que no parece un
servidor, para redes que bloquean la primera. Publica su propio puerto porque
no lleva SNI con el que un proxy inverso pueda enrutar. Los clientes pegan la
línea del puente en Ajustes; ver FN_OBFS4_* en
docs/CONFIGURATION.md en el repositorio del servidor.
pgbouncer solo merece la pena por encima de un
servidor. Un solo servidor abre tan pocas conexiones que Postgres las
atiende directamente. Ponlo como capa sobre prod:
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
-f docker-compose.pooled.yml up -d
Llamadas de voz
Opcional, y solo necesario para las llamadas.
docker-compose.yml, el archivo que usan los pasos de arriba,
no lleva coturn, a propósito: publica en
127.0.0.1 y está pensado para trabajar en el servidor, donde dos
clientes en una misma máquina se alcanzan directamente. Que
/e2e/turn conteste 503 ahí es correcto, no una pieza que falta.
Las llamadas a través de NAT simétrico y CGNAT necesitan un relé en medio,
y ese vive en docker-compose.prod.yml, detrás de un perfil para
que nada relacionado con llamadas se ejecute, ni haya que configurarlo, salvo
que lo pidas:
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile calls up -d
Antes define FN_TURN_HOST, FN_TURN_SECRET y
FN_TURN_EXTERNAL_IP en .env; coturn se niega a
arrancar sin ellas. El servidor acuña credenciales con el mismo secreto y
nunca reenvía el audio. La mensajería funciona sin nada de esto, y con el
perfil apagado /e2e/turn contesta 503, que es un despliegue
soportado y no un fallo.
Mantenerlo al día
Con la imagen publicada: docker compose pull y luego
docker compose up -d.
Construyendo desde el código: git pull y luego
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build.
El --build importa cuando construyes desde el código. Compose
reutiliza una etiqueta de imagen existente en vez de reconstruirla, así que
sin él un git pull baja código nuevo y te deja ejecutando el
binario viejo: sin error, sin aviso, sin actualización. Comprueba qué estás
ejecutando de verdad con docker compose images relay.
Las migraciones se ejecutan al arrancar, así que no hay paso aparte. Son
aditivas: un servidor más nuevo lee una base de datos más antigua y la pone al
día. Haz un volcado antes de todos modos,
docker compose exec db pg_dump -U flamenet flamenet, porque lo
contrario no es cierto, y un servidor antiguo contra una base de datos nueva
no es una dirección soportada.
Avisar a tus usuarios de las actualizaciones
La aplicación de Android le pregunta a tu servidor si existe una compilación más nueva, no a una dirección nuestra, porque una aplicación que dice que no llama a casa no debería pasar lista con nosotros desde cada dispositivo. El efecto secundario es que un servidor que no publica nada no produce ningún aviso de actualización.
Define cuatro variables en .env y tu servidor publica un
manifiesto de versión que la aplicación lee:
FN_ANDROID_VERSION_CODE=4
FN_ANDROID_VERSION_NAME=0.4
FN_ANDROID_SHA256=<sha256 de tu APK> # opcional
FN_ANDROID_NOTES=Qué cambió en esta compilación. # opcional
Sirve tu APK en /downloads/flamenet-messenger.apk en el mismo
anfitrión. La aplicación construye la URL de descarga a partir del origen del
propio servidor e ignora lo que el manifiesto diga sobre dónde vive el
archivo, así que tu servidor puede dirigir a tus usuarios a compilaciones en
sí mismo, y a ninguna otra parte. Deja las variables sin definir y la ruta
sencillamente no existe, que es el valor por defecto correcto: un servidor no
debería inventarse una versión que su operador nunca eligió.
Verificar lo que estás ejecutando
scripts/verify-build.sh # resúmenes de las fuentes del motor
scripts/verify-build.sh --rebuild # demuestra que el ML-KEM incluido se reconstruye byte a byte
Solo, o unido a la federación
Un servidor que ejecutas tú es autónomo por defecto, y seguir así
no requiere ninguna configuración. FN_FEDERATION vale
closed por defecto. Esa elección no desactiva nada: tus usuarios
se registran, intercambian claves, se escriben y verifican tu registro de
transparencia exactamente igual que en cualquier otro sitio. Un servidor
cerrado es un producto completo, no uno recortado.
Y está cerrado de verdad, no de nombre. El servidor tiene una sola pieza
de código de red saliente, el cliente SMTP para el correo de restablecimiento
de contraseña, y está desactivada salvo que configures un servidor de correo.
Con FN_SMTP_HOST sin definir y la federación cerrada, el proceso
no hace ninguna conexión saliente. Habla con su base de datos
y con quien lo llama, y con nadie más. No hay telemetría, ni comprobación de
actualizaciones, ni directorio en el que registrarse.
FN_FEDERATION |
Qué significa |
|---|---|
closed (por defecto) |
Tu servidor habla con tus usuarios y con nadie más. |
allowlist |
Tu servidor intercambia tráfico solo con los pares que nombres. |
open |
Tu servidor acepta pares que no conoce. |
Tu servidor anuncia cuál de estos es en
/.well-known/flamenet/relay, para que un par lea tus condiciones
antes de intentar nada, en vez de descubrirlas por una petición
rechazada.
La federación no es gratis, y el coste son los metadatos. El remitente sellado oculta quién escribe al servidor que lleva el mensaje; no oculta la existencia de tu servidor, ni con qué servidores intercambia tráfico, a los operadores del otro extremo. Unirse a una federación reparte un trozo del grafo social de tus usuarios entre servidores que no ejecutas tú. Es una reducción real de lo que tus usuarios obtienen de que los alojes, y es la razón honesta de que esto venga apagado.
Elígelo a propósito. Si tu servidor existe para que una familia, una
empresa o un grupo de amigos hablen sin nadie más de por medio,
closed no es la opción prudente: es la correcta.
Las identidades funcionan de las dos formas
Una clave de cuenta y el fnid que se deriva de ella merecen la
pena en un servidor cerrado. Son lo que permite a alguien mudarse: la
dirección sale de una clave que el cliente de tu usuario deriva de su frase de
acceso, no de un número de fila que tú asignaste, así que la misma persona
puede registrar la misma identidad en otro sitio y ser reconocida como ella
misma. Eso vale federes o no, incluido si cierras tu servidor, que es
precisamente cuando importa.
Qué te da de verdad montar tu propio servidor
Quita al operador de tu modelo de amenazas, porque el operador eres tú. Nadie más tiene la clave de reposo, a nadie más se le pueden pedir tus datos, y nadie más decide cuándo conservarlos.
No te hace anónimo ante la gente con la que hablas, y no cambia lo que cualquier servidor ve necesariamente: destinatarios, horas y tamaños aproximados. Tampoco resuelve la distribución de claves: tu servidor reparte los paquetes de claves previas, y los números de seguridad siguen siendo la comprobación que no requiere confiar en él.