Abierto Libre y de código abierto, lee el código Descargar Donar Ayuda con la app Contacto
Messenger

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. Conserva issuer.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-For cada 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.

Libre y de código abierto · Sin anuncios · Sin número de teléfono · Tu propio servidor si lo quieres