Files
Warp/server/internal/roomserver/federated.go
T
tajniak81andClaude Opus 5.5 6aaf554336
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 5: federation
Implements PLAN.md Phase 5: the Server-Server API. Warp joins rooms on
other servers and lets their users join its rooms, invites across servers
with the invitee's server countersigning, rejects remote invites (locally
if the inviter is unreachable), and exchanges events, typing, receipts and
to-device messages with other homeservers — all behind the runtime switch,
which now starts and stops real outbound traffic.

The protocol lives in internal/federation with no knowledge of rooms:
X-Matrix signing (fuzzed parser), the spec's discovery algorithm keeping
the dial address and the TLS name apart, a signed client that refuses
every request while the switch is off or the policy denies the
destination, and a key ring that caches other servers' keys for at most a
week, with optional notaries.

The roomserver takes events from elsewhere through the spec's intake:
signatures and hashes (a hash mismatch keeps the redacted form), the rules
against the event's own auth events, against the state before it — with
/get_missing_events for small gaps and /state_ids for wide ones — and
against the current state, where a failure is a soft failure. Events held
without their state are outliers: a column on roomserver_events, because
the current state of a room joined over federation is almost all
outliers, and excluded from every timeline query. Forks are resolved at
the room's head. Membership across servers is make_join/send_join,
make_leave/send_leave and countersigned invites, in both directions;
m.room.server_acl is enforced on everything another server sends.
History before a remote join is backfilled when /messages pages past it,
stored at negative stream positions.

internal/federationapi is the running component: the inbound endpoints,
and an outbound queue that persists what it owes each destination, sends
it in transactions of 50 PDUs and 100 EDUs, backs off from ten seconds to
six hours, survives restarts, and drops events persisted while federation
is off rather than delivering a backlog in one burst. Its Module is what
the switch starts and stops.

Also: a TLS listener for port 8448, trusted key servers, key validity and
an outbound CA bundle in the config; per-destination health and backoff
reset in the admin API; remote profiles, aliases and directories in the
Client-Server API; a Complement federation profile. See
docs/federation.md, including its known gaps (no partial-state joins, no
key rotation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:14:20 +02:00

690 lines
21 KiB
Go

package roomserver
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"sync"
"github.com/dariuszjaniak/warp/server/internal/federation"
"github.com/dariuszjaniak/warp/server/internal/matrix/auth"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
"github.com/dariuszjaniak/warp/server/internal/matrix/stateres"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// The intake of events from other servers.
//
// An event created here is built on the room's current state and can only
// fail the authorisation rules. An event from somewhere else arrives with
// none of that settled: it was built on whatever its sender knew, its auth
// events and its prev_events may be events this server has never seen,
// and it may be perfectly valid where it was made and wrong for the room as
// this server knows it. The spec's procedure for it, which this file
// follows step by step, is:
//
// 1. It is well-formed, signed by its sender's server, and its content
// matches its hash — or it is replaced by its redacted form.
// 2. It passes the authorisation rules against its own auth_events,
// which are fetched if they are missing.
// 3. It passes them against the room's state *before* it — the state
// after its prev_events, resolved when there are several. A prev_event
// this server does not have is asked for with /get_missing_events and,
// if the gap is too wide, replaced by asking for the state at it.
// 4. It passes them against the room's *current* state. One that fails
// only here is soft-failed: kept, so that other events can cite it, but
// never shown to a client and never a forward extremity.
//
// Failing 1 to 3 is a rejection, and a rejected event is not stored.
// Bounds on how much one event can make this server fetch.
const (
// missingEventsLimit is how many events /get_missing_events is asked
// for. A gap wider than this is closed by fetching the state instead.
missingEventsLimit = 10
// stateFetchByIDLimit is how many missing state events are fetched one
// at a time with /event before it is cheaper to ask for the whole
// state with /state.
stateFetchByIDLimit = 50
// fetchConcurrency bounds the /event requests in flight at once.
fetchConcurrency = 8
)
// ReceivePDU takes one PDU from a /send transaction. It returns the event
// ID — which a transaction's response is keyed by — and the reason the
// event was refused, if it was.
func (r *API) ReceivePDU(ctx context.Context, origin string, raw json.RawMessage) (string, error) {
if err := r.requireFederation("receiving events from another server"); err != nil {
return "", err
}
roomID, err := roomIDOfRaw(raw)
if err != nil {
return "", err
}
room, rules, err := r.roomAndRules(ctx, roomID)
if errors.Is(err, ErrNoSuchRoom) {
return "", fmt.Errorf("%w: %s", ErrNotInRoom, roomID)
} else if err != nil {
return "", err
}
e, err := parseRemote(raw, rules)
if err != nil {
return "", err
}
if in, err := r.participating(ctx, roomID); err != nil {
return e.EventID(), err
} else if !in {
return e.EventID(), fmt.Errorf("%w: this server is not in %s", ErrNotInRoom, roomID)
}
if err := r.requireAllowed(ctx, roomID, origin); err != nil {
return e.EventID(), err
}
return e.EventID(), r.receive(ctx, incoming{origin: origin, room: room, rules: rules, event: e, fetchMissing: true})
}
// incoming is one event on its way in.
type incoming struct {
origin string
room storage.Room
rules roomversion.Rules
event *event.Event
// fetchMissing allows /get_missing_events. It is off for the events
// that call itself returns, so that a gap is closed one level deep and
// no further.
fetchMissing bool
// relay marks an event handed over by send_join or send_leave; see
// Notification.Relay.
relay bool
// batch is the part of a backfill batch already accepted, which is
// not stored until the whole batch is (see backfill.go). The events in
// it are this event's likeliest auth and prev events, so they are
// looked for here before the store and before asking anybody.
batch map[string]batchEvent
}
// receive runs the intake procedure.
func (r *API) receive(ctx context.Context, in incoming) error {
// An event already held, as an outlier or in the timeline, has been
// through this before. An outlier is left an outlier even when it
// arrives again as an ordinary event: it is one because this server
// used it as state without having its history, and making it a forward
// extremity now would put the room's head on an event whose children
// may already be here.
if _, err := r.store.Event(ctx, in.room.ID, in.event.EventID()); err == nil {
return nil
} else if !isNotFound(err) {
return err
}
e, err := r.verifyIncoming(ctx, in.event)
if err != nil {
return err
}
in.event = e
// Step 2: the event against its own auth events.
authEvents, err := r.authEventsOf(ctx, in)
if err != nil {
return err
}
create, err := r.createEvent(ctx, in.room, in.rules)
if err != nil {
return err
}
if err := auth.CheckAuthEvents(e, authEvents); err != nil {
return err
}
if err := auth.Check(ctx, e, withCreate(auth.NewState(authEvents), create), r.verifier()); err != nil {
return err
}
// Step 3: the event against the state before it.
stateBefore, err := r.stateBeforeIncoming(ctx, in)
if err != nil {
return err
}
if err := auth.Check(ctx, e, withCreate(stateBefore, create), r.verifier()); err != nil {
return fmt.Errorf("%w (against the state before it)", err)
}
return r.persistIncoming(ctx, in, stateBefore)
}
// withCreate adds the create event to a state that lacks it, which from
// room version 12 is every state built from auth_events.
func withCreate(st auth.State, create *event.Event) auth.State {
if st.Create() == nil && create != nil {
st[auth.StateKey{Type: event.TypeCreate}] = create
}
return st
}
// createEvent reads a room's create event.
func (r *API) createEvent(ctx context.Context, room storage.Room, rules roomversion.Rules) (*event.Event, error) {
stored, err := r.store.CurrentStateEvent(ctx, room.ID, event.TypeCreate, "")
if err != nil {
if isNotFound(err) {
return nil, fmt.Errorf("%w: this server does not hold the state of %s", ErrNotInRoom, room.ID)
}
return nil, err
}
return parseEvent(stored, rules)
}
// persistIncoming is step 4 and the write: under the room lock, the event
// is checked against the current state and either soft-failed or added to
// the room's head.
func (r *API) persistIncoming(ctx context.Context, in incoming, stateBefore auth.State) error {
e := in.event
mu := r.lock(in.room.ID)
mu.Lock()
defer mu.Unlock()
// Another transaction may have delivered the same event while this one
// was fetching what it needed.
if _, err := r.store.Event(ctx, in.room.ID, e.EventID()); err == nil {
return nil
} else if !isNotFound(err) {
return err
}
current, extremities, _, err := r.roomState(ctx, in.room, in.rules)
if err != nil {
return err
}
// Step 4: soft failure.
if err := auth.Check(ctx, e, current, r.verifier()); err != nil {
r.log.Info("soft-failed an event from another server",
"room_id", in.room.ID, "event_id", e.EventID(), "type", e.Type(),
"sender", e.Sender(), "origin", in.origin, "reason", err)
return r.store.WriteOutliers(ctx, in.room.ID, []storage.StoredEvent{storedOf(in.room.ID, e)})
}
// The new forward extremities are the old ones this event does not
// descend from, plus the event.
prevs := e.PrevEventIDs()
var newExtremities []string
for _, x := range extremities {
if !slices.Contains(prevs, x.EventID()) {
newExtremities = append(newExtremities, x.EventID())
}
}
newExtremities = append(newExtremities, e.EventID())
req := persistRequest{
Room: in.room,
Rules: in.rules,
Event: e,
StateBefore: stateBefore,
Extremities: newExtremities,
Origin: in.origin,
Relay: in.relay,
}
if e.Type() == event.TypeRedaction {
req.RedactsEventID = e.Redacts()
}
// The ordinary case: the event follows the room's single extremity, so
// the state before it is the current state and the state after it is
// one tuple more. Anything else — a fork, or an event built on an
// older part of the room — means working the current state out again
// from the room's new head.
linear := len(extremities) == 1 && len(prevs) == 1 && prevs[0] == extremities[0].EventID()
if !linear {
resolved, err := r.stateAtHead(ctx, in.room, in.rules, newExtremities, e, stateBefore)
if err != nil {
return err
}
req.CurrentState = resolved
}
return r.persist(ctx, req)
}
// stateAtHead resolves the room's state across a set of forward
// extremities, one of which is the event being persisted.
func (r *API) stateAtHead(ctx context.Context, room storage.Room, rules roomversion.Rules, extremityIDs []string, e *event.Event, stateBefore auth.State) (auth.State, error) {
var sets [][]*event.Event
for _, id := range extremityIDs {
if id == e.EventID() {
set := stateres.Sorted(stateBefore)
if e.IsState() {
set = append(set, e)
}
sets = append(sets, set)
continue
}
stored, err := r.store.Event(ctx, room.ID, id)
if err != nil {
return nil, err
}
x, err := parseEvent(stored, rules)
if err != nil {
return nil, err
}
set, err := r.stateAfter(ctx, room.ID, rules, x)
if err != nil {
return nil, err
}
sets = append(sets, set)
}
if len(sets) == 1 {
return auth.NewState(sets[0]), nil
}
resolved, err := stateres.Resolve(ctx, rules, sets, eventProvider{store: r.store, roomID: room.ID, rules: rules})
if err != nil {
return nil, fmt.Errorf("resolving the state of %s: %w", room.ID, err)
}
return resolved, nil
}
// authEventsOf returns an incoming event's auth events, fetching the ones
// this server does not hold.
func (r *API) authEventsOf(ctx context.Context, in incoming) ([]*event.Event, error) {
ids := in.event.AuthEventIDs()
have, err := r.heldOrBatched(ctx, in, ids)
if err != nil {
return nil, err
}
if len(have) < len(ids) {
if err := r.fetchAuthChain(ctx, in); err != nil {
return nil, err
}
if have, err = r.heldOrBatched(ctx, in, ids); err != nil {
return nil, err
}
}
if len(have) < len(ids) {
return nil, fmt.Errorf("%w: %s cites auth events that %s could not supply",
ErrBadEvent, in.event.EventID(), in.origin)
}
// Keep the order the event cites them in; CheckAuthEvents does not
// care, but a log line naming them does.
byID := map[string]*event.Event{}
for _, e := range have {
byID[e.EventID()] = e
}
out := make([]*event.Event, 0, len(ids))
for _, id := range ids {
out = append(out, byID[id])
}
return out, nil
}
// heldOrBatched is eventsByID that also finds the events of the backfill
// batch an incoming event belongs to.
func (r *API) heldOrBatched(ctx context.Context, in incoming, ids []string) ([]*event.Event, error) {
var out, rest []string
for _, id := range ids {
if _, ok := in.batch[id]; ok {
out = append(out, id)
} else {
rest = append(rest, id)
}
}
held, err := r.eventsByID(ctx, in.room.ID, in.rules, rest)
if err != nil {
return nil, err
}
for _, id := range out {
held = append(held, in.batch[id].event)
}
return held, nil
}
// eventsByID reads and parses the events of a room this server holds,
// outliers included, leaving out the ones it does not.
func (r *API) eventsByID(ctx context.Context, roomID string, rules roomversion.Rules, ids []string) ([]*event.Event, error) {
if len(ids) == 0 {
return nil, nil
}
stored, err := r.store.EventsByID(ctx, roomID, ids)
if err != nil {
return nil, err
}
return parseEvents(stored, rules)
}
// fetchAuthChain asks the origin for an event's whole auth chain and
// stores what passes as outliers.
func (r *API) fetchAuthChain(ctx context.Context, in incoming) error {
resp, err := r.remote.EventAuth(ctx, in.origin, in.room.ID, in.event.EventID())
if err != nil {
return remoteError(in.origin, err)
}
return r.acceptOutliers(ctx, in.room, in.rules, resp.AuthChain)
}
// acceptOutliers parses, verifies and authorises a batch of events from
// another server, and stores the ones that pass as outliers.
func (r *API) acceptOutliers(ctx context.Context, room storage.Room, rules roomversion.Rules, raws []json.RawMessage) error {
var fresh []*event.Event
referenced := map[string]bool{}
ids := make([]string, 0, len(raws))
for _, raw := range raws {
e, err := parseRemote(raw, rules)
if err != nil {
r.log.Debug("ignoring an unparseable event", "room_id", room.ID, "error", err)
continue
}
if !inRoom(e, room.ID) {
continue
}
ids = append(ids, e.EventID())
fresh = append(fresh, e)
for _, id := range e.AuthEventIDs() {
referenced[id] = true
}
}
// What is already here does not need fetching, verifying or storing
// again; it is only needed as the auth events of what is new.
held, err := r.eventsByID(ctx, room.ID, rules, ids)
if err != nil {
return err
}
heldIDs := map[string]bool{}
for _, e := range held {
heldIDs[e.EventID()] = true
}
var toCheck []*event.Event
for _, e := range fresh {
if !heldIDs[e.EventID()] {
toCheck = append(toCheck, e)
}
}
if len(toCheck) == 0 {
return nil
}
verified := r.verifyAll(ctx, toCheck)
knownIDs := make([]string, 0, len(referenced))
for id := range referenced {
knownIDs = append(knownIDs, id)
}
knownEvents, err := r.eventsByID(ctx, room.ID, rules, knownIDs)
if err != nil {
return err
}
known := map[string]*event.Event{}
for _, e := range knownEvents {
known[e.EventID()] = e
}
if create, err := r.createEvent(ctx, room, rules); err == nil {
known[create.EventID()] = create
}
passed, rejected := r.authCheckChain(ctx, verified, known)
if len(rejected) > 0 {
r.log.Info("rejected events in a fetched auth chain or state",
"room_id", room.ID, "rejected", len(rejected))
}
rows := make([]storage.StoredEvent, 0, len(passed))
for _, e := range verified {
if _, ok := passed[e.EventID()]; ok {
rows = append(rows, storedOf(room.ID, e))
}
}
return r.store.WriteOutliers(ctx, room.ID, rows)
}
// verifyAll checks the signatures and hashes of a batch, in parallel, and
// returns the events that pass in their original order.
func (r *API) verifyAll(ctx context.Context, events []*event.Event) []*event.Event {
out := make([]*event.Event, len(events))
sem := make(chan struct{}, fetchConcurrency)
var wg sync.WaitGroup
for i, e := range events {
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
verified, err := r.verifyIncoming(ctx, e)
if err != nil {
r.log.Debug("dropping an event that does not verify", "event_id", e.EventID(), "error", err)
return
}
out[i] = verified
}()
}
wg.Wait()
return slices.DeleteFunc(out, func(e *event.Event) bool { return e == nil })
}
// stateBeforeIncoming works out the room state an incoming event was built
// on: the state after each of its prev_events, resolved together.
func (r *API) stateBeforeIncoming(ctx context.Context, in incoming) (auth.State, error) {
prevs := in.event.PrevEventIDs()
if len(prevs) == 0 {
return nil, fmt.Errorf("%w: %s has no prev_events", ErrBadEvent, in.event.EventID())
}
// A prev_event earlier in the same backfill batch has its state after
// it in hand already.
var sets [][]*event.Event
var stored []string
for _, id := range prevs {
if b, ok := in.batch[id]; ok {
sets = append(sets, b.stateAfter)
} else {
stored = append(stored, id)
}
}
known, missing, err := r.splitPrevs(ctx, in.room, in.rules, stored)
if err != nil {
return nil, err
}
if len(missing) > 0 && in.fetchMissing {
r.fetchMissingEvents(ctx, in)
if known, missing, err = r.splitPrevs(ctx, in.room, in.rules, stored); err != nil {
return nil, err
}
}
for _, prev := range known {
set, err := r.stateAfter(ctx, in.room.ID, in.rules, prev)
if err != nil {
return nil, err
}
sets = append(sets, set)
}
for _, prevID := range missing {
set, err := r.stateAfterRemote(ctx, in, prevID)
if err != nil {
return nil, err
}
sets = append(sets, set)
}
if len(sets) == 1 {
return auth.NewState(sets[0]), nil
}
resolved, err := stateres.Resolve(ctx, in.rules, sets,
eventProvider{store: r.store, roomID: in.room.ID, rules: in.rules})
if err != nil {
return nil, fmt.Errorf("resolving the state before %s: %w", in.event.EventID(), err)
}
return resolved, nil
}
// splitPrevs sorts prev_events into the ones in this server's timeline,
// which have a state snapshot, and the ones it lacks. An outlier counts as
// lacking: it is held, but the state at it is not.
func (r *API) splitPrevs(ctx context.Context, room storage.Room, rules roomversion.Rules, prevs []string) ([]*event.Event, []string, error) {
stored, err := r.store.EventsByID(ctx, room.ID, prevs)
if err != nil {
return nil, nil, err
}
have := map[string]storage.StoredEvent{}
for _, s := range stored {
if !s.Outlier {
have[s.EventID] = s
}
}
var known []*event.Event
var missing []string
for _, id := range prevs {
s, ok := have[id]
if !ok {
missing = append(missing, id)
continue
}
e, err := parseEvent(s, rules)
if err != nil {
return nil, nil, err
}
known = append(known, e)
}
return known, missing, nil
}
// fetchMissingEvents asks the origin for the events between this server's
// head of the room and the incoming event, and takes each of them in,
// oldest first. Failures are logged, not returned: whatever this closes of
// the gap helps, and what it leaves open is closed by fetching state.
func (r *API) fetchMissingEvents(ctx context.Context, in incoming) {
stored, err := r.store.Extremities(ctx, in.room.ID)
if err != nil {
r.log.Warn("could not read extremities to fetch missing events", "room_id", in.room.ID, "error", err)
return
}
earliest := make([]string, 0, len(stored))
var minDepth int64
for i, s := range stored {
earliest = append(earliest, s.EventID)
if i == 0 || s.Depth < minDepth {
minDepth = s.Depth
}
}
resp, err := r.remote.MissingEvents(ctx, in.origin, in.room.ID, federation.MissingEventsRequest{
EarliestEvents: earliest,
LatestEvents: []string{in.event.EventID()},
Limit: missingEventsLimit,
MinDepth: minDepth,
})
if err != nil {
r.log.Info("could not fetch missing events", "room_id", in.room.ID,
"origin", in.origin, "event_id", in.event.EventID(), "error", err)
return
}
var events []*event.Event
for _, raw := range resp.Events {
e, err := parseRemote(raw, in.rules)
if err != nil || e.RoomID() != in.room.ID || e.EventID() == in.event.EventID() {
continue
}
events = append(events, e)
}
eventsByDepth(events)
for _, e := range events {
err := r.receive(ctx, incoming{origin: in.origin, room: in.room, rules: in.rules, event: e})
if err != nil {
r.log.Info("a fetched missing event was not accepted",
"room_id", in.room.ID, "event_id", e.EventID(), "error", err)
}
}
}
// stateAfterRemote asks the origin for the state at a prev_event this
// server does not have, fetches whatever of it is missing, and returns the
// state after that event.
//
// /state_ids answers with the state *before* the event it is asked about,
// so the event itself is fetched too and, if it is a state event, added on
// top. Everything fetched is authorised against its own auth events and
// stored as an outlier: it is real, but this server has none of the
// history that would put it in the timeline.
func (r *API) stateAfterRemote(ctx context.Context, in incoming, prevID string) ([]*event.Event, error) {
ids, err := r.remote.StateIDs(ctx, in.origin, in.room.ID, prevID)
if err != nil {
return nil, remoteError(in.origin, err)
}
need := append(append(slices.Clone(ids.PDUIDs), ids.AuthChainIDs...), prevID)
have, err := r.store.EventsByID(ctx, in.room.ID, need)
if err != nil {
return nil, err
}
haveIDs := map[string]bool{}
for _, s := range have {
haveIDs[s.EventID] = true
}
var missing []string
for _, id := range need {
if !haveIDs[id] && !slices.Contains(missing, id) {
missing = append(missing, id)
}
}
if len(missing) > 0 {
var raws []json.RawMessage
if len(missing) > stateFetchByIDLimit {
state, err := r.remote.State(ctx, in.origin, in.room.ID, prevID)
if err != nil {
return nil, remoteError(in.origin, err)
}
raws = append(append(raws, state.AuthChain...), state.PDUs...)
if !slices.Contains(missing, prevID) {
missing = nil
} else {
missing = []string{prevID}
}
}
raws = append(raws, r.fetchEvents(ctx, in.origin, missing)...)
if err := r.acceptOutliers(ctx, in.room, in.rules, raws); err != nil {
return nil, err
}
}
state, err := r.eventsByID(ctx, in.room.ID, in.rules, ids.PDUIDs)
if err != nil {
return nil, err
}
prev, err := r.eventsByID(ctx, in.room.ID, in.rules, []string{prevID})
if err != nil {
return nil, err
}
if len(prev) == 1 && prev[0].IsState() {
state = append(state, prev[0])
}
return state, nil
}
// fetchEvents fetches events one by one from a server, in parallel,
// returning the ones it supplied.
func (r *API) fetchEvents(ctx context.Context, server string, ids []string) []json.RawMessage {
results := make([]json.RawMessage, len(ids))
sem := make(chan struct{}, fetchConcurrency)
var wg sync.WaitGroup
for i, id := range ids {
wg.Add(1)
sem <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-sem }()
txn, err := r.remote.Event(ctx, server, id)
if err != nil || len(txn.PDUs) == 0 {
r.log.Debug("could not fetch an event", "server", server, "event_id", id, "error", err)
return
}
results[i] = txn.PDUs[0]
}()
}
wg.Wait()
return slices.DeleteFunc(results, func(raw json.RawMessage) bool { return raw == nil })
}