Featured

Matrix Federation: Налаштування та діагностика

Federation дозволяє користувачам з різних Matrix серверів спілкуватися між собою. @karas:eliah.one може писати @cetym:eliah-b.one — хоча вони на різних серверах. Цей мануал описує покрокове налаштування federation між двома Synapse серверами з використанням Caddy як reverse proxy.

⚠️ Практичний досвід
Цей мануал написаний на основі реального налаштування federation між вузлами UMTC у квітні 2026. Кожна секція "Пастки" — це реальна проблема, яку ми зустріли та вирішили.

Передумови

Перед налаштуванням federation переконайтесь що маєте:

  • Два працюючих Synapse сервери з різними server_name
  • Caddy як reverse proxy на обох серверах
  • Публічні DNS записи для обох доменів
  • TLS сертифікати (Caddy отримує автоматично через Let's Encrypt)
  • Порт 8448 відкритий на файрволі для вхідних з'єднань

Архітектура

flowchart LR
    subgraph nodeA["Вузол A"]
        CA["Caddy :8448<br/>protocols h1"]
        SA["Synapse A<br/>server_name: server-a.example"]
    end

    subgraph nodeB["Вузол B"]
        CB["Caddy :8448<br/>protocols h1"]
        SB["Synapse B<br/>server_name: server-b.example"]
    end

    CA <-->|"Federation API<br/>HTTP/1.1 :8448"| CB
    SA --> CA
    SB --> CB

    style nodeA fill:#d1fae5
    style nodeB fill:#dbeafe

Крок 1: DNS

Кожен сервер потребує публічні DNS записи. Зовнішні сервери повинні знати куди стукатися.

server-a.example       A    203.0.113.10
matrix.server-a.example  A    203.0.113.10

server-b.example       A    198.51.100.20
matrix-b.server-b.example  A    198.51.100.20
💡 Перевірка
dig +short server-a.example A
dig +short matrix-b.server-b.example A
Обидва мають повертати публічні IP.

Пастка: dnsmasq override

Якщо на сервері є dnsmasq з override записами (наприклад для WireGuard routing), Synapse у Docker контейнері може резолвити hostname у приватну IP адресу замість публічної.

Чому це проблема: Synapse має вбудований SSRF захист і блокує з'єднання до приватних IP (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). Якщо DNS резолвить federation target у приватну IP — Synapse відмовить.

Лог виглядає так:

Blocked 10.10.11.1 from DNS resolution to matrix-b.server-b.example

Перевірте:

# Перевірити dnsmasq overrides
cat /etc/dnsmasq.d/*.conf | grep -i "matrix\|server-b"

# Перевірити що Docker контейнер резолвить у публічну IP
docker exec synapse getent hosts matrix-b.server-b.example
# Має бути: 198.51.100.20  (публічна IP)

Рішення — видалити override та перезапустити:

sed -i '/address=\/matrix-b\.server-b\.example/d' /etc/dnsmasq.d/matrix.conf
systemctl restart dnsmasq
docker restart synapse

Крок 2: Well-known

Matrix federation починається з discovery. Сервер A хоче знайти сервер B — він звертається до https://server-b.example/.well-known/matrix/server.

Caddy конфігурація

server-b.example {
    bind 198.51.100.20
    header /.well-known/matrix/* Content-Type application/json
    respond /.well-known/matrix/server `{"m.server": "matrix-b.server-b.example:8448"}`
    respond /.well-known/matrix/client `{"m.homeserver": {"base_url": "https://matrix-b.server-b.example"}}`
}
⚠️ Порт у well-known
Вказуйте порт **8448** у `m.server`, не 443. Причина описана у секції "Пастка: HTTP/2 та Twisted" — federation клієнт Synapse має проблеми з HTTP/2, який увімкнений на порту 443.

Перевірка

curl -s https://server-b.example/.well-known/matrix/server | python3 -m json.tool
# {"m.server": "matrix-b.server-b.example:8448"}

Крок 3: Caddy — Federation endpoint

Federation API працює на порту 8448. Caddy має слухати на цьому порту і проксувати запити до Synapse.

# Federation endpoint — публічний!
matrix-b.server-b.example:8448, server-b.example:8448 {
    bind 198.51.100.20
    reverse_proxy /_matrix/* localhost:8008
    reverse_proxy /_synapse/* localhost:8008
}

Обов'язково bind на публічну IP — federation приходить ззовні.

Критично: вимкнути HTTP/2 на порту 8448

Synapse використовує Twisted HTTP клієнт для federation. Twisted має несумісність з HTTP/2 — при h2 з'єднанні він не бачить Content-Type header у відповіді і падає з помилкою.

Додайте у global options Caddyfile:

{
    servers 198.51.100.20:8448 {
        protocols h1
    }
}
⚠️ Matcher для servers
`servers :8448 { protocols h1 }` **не працює** якщо є site blocks з конкретними hostnames на порту 8448. Caddy створює окремі listener для кожної bind адреси. Використовуйте конкретну адресу. Перевірте через Caddy admin API:
curl -s localhost:2019/config/apps/http/servers/ | \
  python3 -m json.tool | grep -A3 -iE "listen|protocols"
# Кожен listener на :8448 має мати "protocols": ["h1"]

Перевірка

# Має бути HTTP/1.1, НЕ HTTP/2
curl -sv https://server-b.example:8448/_matrix/key/v2/server 2>&1 | grep -iE "alpn|HTTP"
# Очікуємо: ALPN: server accepted http/1.1
#           HTTP/1.1 200 OK

Пастка: HTTP/2 та Twisted — детально

Симптоми у логах Synapse (на сервері що намагається отримати ключі):

Error reading response GET matrix-federation://server-a.example/_matrix/key/v2/server: 
Failed to send request: RuntimeError: No Content-Type header received from remote server

При цьому curl з того самого контейнера працює:

docker exec synapse curl -s https://server-a.example:8448/_matrix/key/v2/server
# JSON ✓ — curl це robust h2 client

А curl --http1.1 теж працює:

docker exec synapse curl -s --http1.1 https://server-a.example:8448/_matrix/key/v2/server
# JSON ✓

Проблема саме у Twisted HTTP клієнті, який не коректно парсить HTTP/2 response headers від Caddy. Після вимкнення h2 на порту 8448 — Twisted отримує нормальний HTTP/1.1 response і все працює.

Крок 4: Synapse homeserver.yaml

Базові federation налаштування

server_name: "server-b.example"

# Ключ підпису — генерується автоматично при першому запуску
# НІКОЛИ не змінюйте після першого запуску!
signing_key_path: "/data/server-b.example.signing.key"

# Federation whitelist — список серверів з якими дозволена federation
# Якщо відсутній — дозволена federation з усіма серверами
federation_domain_whitelist:
  - server-a.example
  - matrix.org

# Trusted key servers для верифікації signing keys
trusted_key_servers:
  - server_name: "matrix.org"

Пастка: federation_ip_range_whitelist

Якщо у homeserver.yaml є federation_ip_range_whitelist або ip_range_whitelist — це обмежить outbound federation ТІЛЬКИ до вказаних IP діапазонів.

# ❌ Це обмежить federation тільки до приватної підмережі
federation_ip_range_whitelist:
  - '10.10.11.0/24'
ip_range_whitelist:
  - '10.10.11.0/24'

# ✅ Видаліть ці параметри якщо federation target має публічну IP

Симптом ідентичний HTTP/2 проблемі ("No Content-Type header"), бо Synapse обриває з'єднання до забороненої IP ще до отримання response.

Signing Keys

Кожен Synapse має унікальний signing key. Коли сервер A надсилає federation запит серверу B, запит підписується ключем A. Сервер B верифікує підпис через endpoint /_matrix/key/v2/server.

curl -s https://server-a.example:8448/_matrix/key/v2/server | python3 -m json.tool
# Має містити:
# "server_name": "server-a.example",
# "verify_keys": {"ed25519:a_xykP": {"key": "..."}}
# "valid_until_ts": 1776964675668

Якщо Synapse B не може отримати ключ A — він спробує через matrix.org як notary:

Requesting keys [...] from notary server matrix.org

Якщо і notary не допоможе — буде 401 Unauthorized.

Крок 5: Фаєрвол

Порт 8448/tcp має бути відкритий для вхідних з'єднань.

# UFW
ufw allow 8448/tcp

# iptables
iptables -A INPUT -p tcp --dport 8448 -j ACCEPT

Діагностика

Federation Tester

https://federationtester.matrix.org/api/report?server_name=server-b.example

Чек-лист з нуля

  1. ☐ DNS записи резолвляться в публічні IP
  2. ☐ Well-known endpoint повертає JSON з m.server на порту 8448
  3. ☐ Caddy слухає на :8448 з bind на публічну IP
  4. ☐ HTTP/2 вимкнений на :8448 (global servers з protocols h1)
  5. federation_domain_whitelist містить потрібні домени
  6. ☐ Немає federation_ip_range_whitelist що блокує публічні IP
  7. ☐ Signing key endpoint доступний ззовні через HTTP/1.1
  8. ☐ dnsmasq не override-ить federation targets у приватні IP
  9. ☐ Docker контейнер резолвить правильні IP (getent hosts)
  10. ☐ Порт 8448 відкритий на файрволі
  11. curl -sv показує ALPN: server accepted http/1.1

Типові помилки

Лог Причина Рішення
Blocked 10.x.x.x from DNS resolution dnsmasq override → приватна IP Видалити override
No Content-Type header received HTTP/2 на :443 або :8448, або ip_range_whitelist h1 на :8448 + well-known → :8448
401 Unauthorized + Failed to find any key Не може отримати signing key Перевірити /_matrix/key/v2/server
403 Federation denied with X Домен не в whitelist Додати в federation_domain_whitelist
Connection refused :8448 Порт не слухає Перевірити Caddy bind + firewall
DNS lookup failed DNS не резолвить Перевірити A-записи

Корисні команди

# Перевірка з Docker контейнера
docker exec synapse getent hosts matrix-b.server-b.example
docker exec synapse curl -sv https://server-a.example:8448/_matrix/key/v2/server

# Tail federation логів
docker logs -f synapse 2>&1 | \
  grep -iE "federation|invite|key|well-known|blocked" | grep -v "prev group"

# Caddy протоколи
curl -s localhost:2019/config/apps/http/servers/ | \
  python3 -m json.tool | grep -A3 "listen\|protocols"

Пов'язані теми

Шлях: services/matrix/federation-setup.md

UMTC Wiki © 2026 | Ukrainian Military Tactical Communications