Skip 503s in error reports, stamp a build-time release, list every outbound connection

503 is what a draining relay answers during a planned restart, so it no
longer becomes an event. Every other 5xx still does, and an error logged
while answering a 503 is still reported.

FN_RELEASE, baked into the image by `--build-arg FN_RELEASE=<sha>`
(Dockerfile, and passed through by Scripts/staging-gate.sh), is sent as
the event's release. Unset, empty or not a plain version string, and
events carry none.

The configuration, hosting and self-hosting pages, and the Spanish
self-hosting page, said SMTP was the relay's only outbound code. The
relay also dials Apple's push service, peer relays, Stripe, an S3 store
and now the error collector. Each is listed with the setting that turns
it on, ending on what a relay with all of them unset opens: nothing.
self-hosting.leaf is generated from flamenet-e2e SELF-HOSTING.md, so
.e2e-ref moves to 310c315, that change made on the pinned commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-10-13 11:24:38 -04:00
co-authored by Claude Opus 5.5
parent 98dcc210ad
commit 1f8cf1ef82
12 changed files with 286 additions and 55 deletions
+1 -1
View File
@@ -1 +1 @@
50fcc9992e81b294091e23ee1a72923899d4db1c
310c315155833ef964962d55d74b6bb9faa3ab49
+6
View File
@@ -41,6 +41,12 @@ ENV FN_DB=/data/relay.sqlite \
FN_ATTACHMENTS_DIR=/data/attachments
VOLUME ["/data"]
# The commit this image was built from, for error reports
# (`--build-arg FN_RELEASE=<sha>`). Last, so a new sha does not invalidate
# the layers above. Unset leaves it empty and reports carry no release.
ARG FN_RELEASE=
ENV FN_RELEASE=${FN_RELEASE}
USER relay
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=5 \
+47 -16
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/CONFIGURATION.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: 9d20067215874430 -->
source-sha256: 6d54d991e7466090 -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="configuring-a-relay">Configuring a relay</h1>
@@ -264,17 +264,28 @@ it logs request URLs, and a protocol URL contains a device id.</td>
<td>Error reporting to a Sentry-protocol collector you run (GlitchTip),
as
<code>http://&lt;key&gt;@&lt;host&gt;:&lt;port&gt;/&lt;project&gt;</code>.
Unset sends nothing. Set, errors, criticals and 5xx responses each
become one event: the message with account ids, device ids, addresses,
tokens, emails and phone numbers removed, the error type, the route
template (<code>/e2e/devices/:deviceID</code>, never the path asked
for), method, status and the source line. No headers, bodies, query
strings, addresses or logger metadata. Warnings stay on stdout; info and
debug are never sent. At most 30 events a minute, one per fault per 60
seconds, 2 second timeout, no retries. The collector keeps what it
receives for as long as it is configured to, so point it at one whose
retention you have chosen. A value that is not a DSN stops the relay at
start-up.</td>
Unset sends nothing. Set, errors, criticals and 5xx responses other than
503 (what a draining relay answers during a restart) each become one
event: the message with account ids, device ids, addresses, tokens,
emails and phone numbers removed, the error type, the route template
(<code>/e2e/devices/:deviceID</code>, never the path asked for), method,
status and the source line. No headers, bodies, query strings, addresses
or logger metadata. Warnings stay on stdout; info and debug are never
sent. At most 30 events a minute, one per fault per 60 seconds, 2 second
timeout, no retries. The collector keeps what it receives for as long as
it is configured to, so point it at one whose retention you have chosen.
A value that is not a DSN stops the relay at start-up.</td>
</tr>
<tr>
<td><code>FN_RELEASE</code></td>
<td>unset</td>
<td>The release error reports are tagged with. Baked into the image at
build time, normally the commit sha:
<code>docker build --build-arg FN_RELEASE=$(git rev-parse --short HEAD)</code>,
or <code>FN_RELEASE=&lt;sha&gt; Scripts/staging-gate.sh</code>, which
passes it on. Letters, digits and <code>._-+</code>, at most 64
characters; anything else, or unset, and reports carry no release. Read
only when <code>FN_SENTRY_DSN</code> is set.</td>
</tr>
</tbody>
</table>
@@ -334,10 +345,30 @@ graph is spread across operators you do not run. That is the honest
reason this is off by default. If your relay exists so one group can
talk without anyone else involved, <code>closed</code> is the correct
answer, not the cautious one.</p>
<p>A closed relay opens no outbound connections at all. The only
outbound code in the relay is the SMTP client below, itself disabled
unless configured. No telemetry, no update check, no directory to
register with.</p>
<p>The relay opens an outbound connection only for a feature you have
switched on. Each one, and the setting that turns it on:</p>
<ul>
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
Off unless set. It sends only to that address, and only scrubbed errors
and 5xx responses other than 503.</li>
<li>iOS wake-ups, to Apple's push service, when
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
<code>FN_APNS_TEAM_ID</code> are all set.</li>
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
<code>allowlist</code> or <code>open</code>. Sending to a peer also
needs <code>FN_FEDERATION_KEY</code>.</li>
<li>Donations, to <code>api.stripe.com</code>, when
<code>FN_DONATE_SECRET</code> is set.</li>
<li>Attachments, to an S3-compatible store, when
<code>FN_S3_ENDPOINT</code> is set.</li>
</ul>
<p>It also connects to the databases you point it at: Postgres at
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
Nothing reports to this project: there is no telemetry, no update check
and no directory to register with. With every setting above unset, the
relay keeps its data in a local SQLite file and opens no outbound
connections.</p>
<h2 id="shared-flamenet-accounts">Shared FlameNet accounts</h2>
<p>Only for a relay that accepts identities from a FlameNet account
service. Leaving all of these unset gives you a relay with its own user
+27 -7
View File
@@ -289,13 +289,33 @@ no requiere ninguna configuración.</strong> <code>FN_FEDERATION</code> vale
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.</p>
<p>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 <code>FN_SMTP_HOST</code> sin definir y la federación cerrada, el proceso
<strong>no hace ninguna conexión saliente</strong>. 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.</p>
<p>Y está cerrado de verdad, no de nombre. El servidor solo abre una
conexión hacia fuera para una función que hayas activado. Estas son todas, con
el ajuste que enciende cada una:</p>
<ul>
<li>El correo para restablecer la contraseña, por SMTP hacia
<code>FN_SMTP_HOST</code>.</li>
<li>Los informes de errores, hacia el recolector indicado en
<code>FN_SENTRY_DSN</code>. Están apagados mientras no lo definas. Solo se
envían a esa dirección, y solo contienen errores ya limpios de identificadores
y respuestas 5xx que no sean 503.</li>
<li>Los avisos que despiertan a un iPhone, hacia el servicio de notificaciones
de Apple, cuando están definidos <code>FN_APNS_KEY_P8</code>,
<code>FN_APNS_KEY_ID</code> y <code>FN_APNS_TEAM_ID</code>.</li>
<li>La federación, hacia otros servidores, cuando <code>FN_FEDERATION</code>
vale <code>allowlist</code> u <code>open</code>. Para enviar a otro servidor
hace falta además <code>FN_FEDERATION_KEY</code>.</li>
<li>Las donaciones, hacia <code>api.stripe.com</code>, cuando está definido
<code>FN_DONATE_SECRET</code>.</li>
<li>Los adjuntos, hacia un almacenamiento compatible con S3, cuando está
definido <code>FN_S3_ENDPOINT</code>.</li>
</ul>
<p>También se conecta a las bases de datos que le indiques: Postgres en
<code>FN_POSTGRES_URL</code> y Redis en <code>FN_REDIS_URL</code>. Nada
informa a este proyecto: no hay telemetría, ni comprobación de
actualizaciones, ni directorio en el que registrarse. Con todos esos ajustes
sin definir, el servidor guarda sus datos en un archivo SQLite local y no abre
ninguna conexión hacia fuera.</p>
<table>
<thead>
<tr>
+25 -4
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from docs/HOSTING.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: da3efb24c74a2759 -->
source-sha256: daf5dd4c607184fc -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="running-your-own-relay">Running your own relay</h1>
@@ -133,9 +133,30 @@ forever. Migrate the account service first, then the relays.</p>
<pre><code>FN_FEDERATION=allowlist
FN_FEDERATION_PEERS=relay.friend.example
FN_FEDERATION_KEY=&lt;base64 Ed25519 seed&gt;</code></pre>
<p>A closed relay opens no outbound connections at all. No telemetry, no
update check, no directory to register with. The only outbound code is
the SMTP client, itself disabled unless configured.</p>
<p>The relay opens an outbound connection only for a feature you have
switched on. Each one, and the setting that turns it on:</p>
<ul>
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
Off unless set. It sends only to that address, and only scrubbed errors
and 5xx responses other than 503.</li>
<li>iOS wake-ups, to Apple's push service, when
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
<code>FN_APNS_TEAM_ID</code> are all set.</li>
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
<code>allowlist</code> or <code>open</code>. Sending to a peer also
needs <code>FN_FEDERATION_KEY</code>.</li>
<li>Donations, to <code>api.stripe.com</code>, when
<code>FN_DONATE_SECRET</code> is set.</li>
<li>Attachments, to an S3-compatible store, when
<code>FN_S3_ENDPOINT</code> is set.</li>
</ul>
<p>It also connects to the databases you point it at: Postgres at
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
Nothing reports to this project: there is no telemetry, no update check
and no directory to register with. With every setting above unset, the
relay keeps its data in a local SQLite file and opens no outbound
connections.</p>
<p><strong>Federation costs metadata.</strong> Each relay in an exchange
learns the other had traffic for it, so a slice of your users' social
graph is spread across operators you do not run. If your relay exists so
+26 -8
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/SELF-HOSTING.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: 38f049224fdd009e -->
source-sha256: 5f3dd761a60d7f48 -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="self-hosting">Self-hosting</h1>
@@ -330,13 +330,31 @@ disabled by that choice: your users register, exchange keys, message
each other, and verify your transparency log exactly as they would
anywhere else. A closed relay is a complete product, not a crippled
one.</p>
<p>It is also closed rather than nominally closed. The relay has one
piece of outbound network code in it, the SMTP client for password reset
mail, and that is disabled unless you configure a mail server. With
<code>FN_SMTP_HOST</code> unset and federation closed, the process makes
<strong>no outbound connections at all</strong>. It talks to its
database and to whoever dials it, and to nothing else. There is no
telemetry, no update check, and no directory to register with.</p>
<p>It is also closed rather than nominally closed. The relay opens an
outbound connection only for a feature you have switched on. Each one,
and the setting that turns it on:</p>
<ul>
<li>Password-reset mail, over SMTP to <code>FN_SMTP_HOST</code>.</li>
<li>Error reports, to the collector named in <code>FN_SENTRY_DSN</code>.
Off unless set. It sends only to that address, and only scrubbed errors
and 5xx responses other than 503.</li>
<li>iOS wake-ups, to Apple's push service, when
<code>FN_APNS_KEY_P8</code>, <code>FN_APNS_KEY_ID</code> and
<code>FN_APNS_TEAM_ID</code> are all set.</li>
<li>Federation, to peer relays, when <code>FN_FEDERATION</code> is
<code>allowlist</code> or <code>open</code>. Sending to a peer also
needs <code>FN_FEDERATION_KEY</code>.</li>
<li>Donations, to <code>api.stripe.com</code>, when
<code>FN_DONATE_SECRET</code> is set.</li>
<li>Attachments, to an S3-compatible store, when
<code>FN_S3_ENDPOINT</code> is set.</li>
</ul>
<p>It also connects to the databases you point it at: Postgres at
<code>FN_POSTGRES_URL</code> and Redis at <code>FN_REDIS_URL</code>.
Nothing reports to this project: there is no telemetry, no update check
and no directory to register with. With every setting above unset, the
relay keeps its data in a local SQLite file and opens no outbound
connections.</p>
<table>
<thead>
<tr>
+4 -1
View File
@@ -46,7 +46,10 @@ trap cleanup EXIT
fail() { echo "GATE FAILED: $1"; echo "--- candidate log tail ---"; docker logs --tail 40 ${STACK}-relay 2>&1 || true; exit 1; }
echo "GATE: building candidate image"
docker build -t flamenet-relay:candidate ./src >/dev/null
# FN_RELEASE (the commit sha being shipped) is baked into the image for error
# reports: `sudo FN_RELEASE=<sha> ./src/Scripts/staging-gate.sh`. ./src is a
# copy, not a checkout, so the sha cannot be read from it here.
docker build --build-arg FN_RELEASE="${FN_RELEASE:-}" -t flamenet-relay:candidate ./src >/dev/null
echo "GATE: booting staging stack (throwaway db + redis, candidate relay)"
cleanup
+1
View File
@@ -1643,6 +1643,7 @@ func errorReporter(for app: Application) throws -> ErrorReporter? {
guard let dsn else { return nil }
let reporter = ErrorReporter(
dsn: dsn, environment: app.environment.name,
release: ErrorReporting.releaseFromEnvironment(),
transport: VaporClientTransport(
client: app.client, logger: Logger(label: "flamenet.error-reporter")))
ErrorReporting.install(reporter)
+34 -8
View File
@@ -8,7 +8,9 @@ import Vapor
// Error reporting to an operator-run Sentry-protocol collector (GlitchTip).
//
// Off unless FN_SENTRY_DSN is set. When it is, errors and criticals, and every
// response with a 5xx status, become one event each. Warnings stay on stdout.
// response with a 5xx status other than 503, become one event each. 503 is
// what a draining relay answers during a planned restart, so it is not a
// fault. Warnings stay on stdout.
// Info and debug are never sent, because the `debug` level is the one that
// writes request URLs and a protocol URL names a device.
//
@@ -288,6 +290,7 @@ enum SentryEnvelope {
let level: String
let logger: String?
let environment: String
let release: String?
let message: Message
let exception: Exception?
let tags: [String: String]
@@ -305,8 +308,8 @@ enum SentryEnvelope {
}
static func encode(
_ event: ReportEvent, environment: String, eventID: String = UUID().uuidString,
now: Date = Date()
_ event: ReportEvent, environment: String, release: String? = nil,
eventID: String = UUID().uuidString, now: Date = Date()
) throws -> Data {
let id = eventID.replacingOccurrences(of: "-", with: "").lowercased()
let iso = ISO8601DateFormatter()
@@ -328,7 +331,7 @@ enum SentryEnvelope {
])
let payload = Payload(
event_id: id, timestamp: stamp, level: sentryLevel(event.level),
logger: event.logger, environment: environment,
logger: event.logger, environment: environment, release: release,
message: Message(formatted: event.message), exception: exception,
tags: tags, fingerprint: [event.fingerprint])
@@ -386,6 +389,7 @@ struct VaporClientTransport: ErrorTransport {
final class ErrorReporter: @unchecked Sendable {
let dsn: SentryDSN
let environment: String
let release: String?
private let transport: any ErrorTransport
private let now: @Sendable () -> Date
@@ -400,12 +404,13 @@ final class ErrorReporter: @unchecked Sendable {
private var recent: [String: Date] = [:]
init(
dsn: SentryDSN, environment: String, transport: any ErrorTransport,
perMinute: Int = 30, dedupeWindow: TimeInterval = 60,
dsn: SentryDSN, environment: String, release: String? = nil,
transport: any ErrorTransport, perMinute: Int = 30, dedupeWindow: TimeInterval = 60,
now: @escaping @Sendable () -> Date = { Date() }
) {
self.dsn = dsn
self.environment = environment
self.release = release
self.transport = transport
self.now = now
self.capacity = Double(perMinute)
@@ -423,7 +428,8 @@ final class ErrorReporter: @unchecked Sendable {
guard admit(event.fingerprint) else { return false }
let body: Data
do {
body = try SentryEnvelope.encode(event, environment: environment, now: now())
body = try SentryEnvelope.encode(
event, environment: environment, release: release, now: now())
} catch {
return false
}
@@ -493,6 +499,19 @@ enum ErrorReporting {
lock.unlock()
}
/// FN_RELEASE, baked into the image at build time (the commit sha, as a
/// Docker build argument). Unset, empty, or not a plain version string:
/// nil, and events carry no release.
static func releaseFromEnvironment(
_ env: [String: String] = ProcessInfo.processInfo.environment
) -> String? {
guard let raw = env["FN_RELEASE"]?.trimmingCharacters(in: .whitespaces),
!raw.isEmpty, raw.count <= 64,
raw.allSatisfy({ $0.isASCII && ($0.isLetter || $0.isNumber || "._-+".contains($0)) })
else { return nil }
return raw
}
/// Reads FN_SENTRY_DSN. Unset or empty: nil, nothing is sent. Set but
/// unparseable: refuse to start, like every other malformed setting.
static func dsnFromEnvironment(
@@ -667,7 +686,7 @@ struct ErrorReportingMiddleware: AsyncMiddleware {
request.logger[metadataKey: RequestReportContext.metadataKey] = nil
request.storage[RequestReportContext.StorageKey.self] = nil
let seen = context.snapshot
guard status >= 500 || seen.level != nil else { return }
guard Self.statusIsReported(status) || seen.level != nil else { return }
let values = Self.identifyingValues(request)
let message =
seen.message.map { ReportScrubber.scrub($0, replacing: values) }
@@ -680,6 +699,13 @@ struct ErrorReportingMiddleware: AsyncMiddleware {
function: seen.function, line: seen.line, logger: "codes.vapor.request"))
}
/// Every 5xx but 503, which a draining relay answers on purpose. An
/// error logged while answering a 503 is still reported: the log line
/// is the fault, not the status.
static func statusIsReported(_ status: UInt) -> Bool {
status >= 500 && status != 503
}
/// Route parameters and query values of this request: removed from the
/// message by value, so a username is caught even though nothing about
/// its shape gives it away.
@@ -289,8 +289,8 @@ struct ErrorReportingTests {
func statusOnlyReporting() async throws {
let transport = RecordingTransport()
try await withReportingApp(transport) { app in
try await app.testing().test(.GET, "/unavailable") { res in
#expect(res.status == .serviceUnavailable)
try await app.testing().test(.GET, "/bad-gateway") { res in
#expect(res.status == .badGateway)
}
try await app.testing().test(.GET, "/no/such/\(Self.accountID)") { res in
#expect(res.status == .notFound)
@@ -298,11 +298,76 @@ struct ErrorReportingTests {
try await transport.waitFor(1)
try await Task.sleep(for: .milliseconds(100))
#expect(transport.count == 1)
#expect(transport.bodies[0].contains(#""status":"503""#))
#expect(transport.bodies[0].contains(#""status":"502""#))
#expect(!Self.content(transport.bodies[0]).contains(Self.accountID))
}
}
@Test("a 503, what a draining relay answers during a deploy, is not reported")
func serviceUnavailableIsNotReported() async throws {
#expect(!ErrorReportingMiddleware.statusIsReported(503))
for status: UInt in [500, 501, 502, 504, 507, 599] {
#expect(ErrorReportingMiddleware.statusIsReported(status))
}
for status: UInt in [200, 404, 429, 499] {
#expect(!ErrorReportingMiddleware.statusIsReported(status))
}
let transport = RecordingTransport()
try await withReportingApp(transport) { app in
for _ in 0..<3 {
try await app.testing().test(.GET, "/unavailable") { res in
#expect(res.status == .serviceUnavailable)
}
}
try await Task.sleep(for: .milliseconds(200))
#expect(transport.count == 0)
// An error logged while answering 503 is a fault of its own.
try await app.testing().test(.GET, "/unavailable-with-error") { res in
#expect(res.status == .serviceUnavailable)
}
try await transport.waitFor(1)
#expect(transport.count == 1)
#expect(transport.bodies[0].contains("redis went away"))
}
}
// MARK: - Release
@Test("FN_RELEASE becomes the release; unset, empty or malformed sends none")
func releaseFromEnvironment() throws {
#expect(ErrorReporting.releaseFromEnvironment([:]) == nil)
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": ""]) == nil)
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": " 98dcc21 "]) == "98dcc21")
#expect(
ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "flamenet-relay@1.2.3+98dcc21"])
== nil)
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "1.2.3+98dcc21"]) == "1.2.3+98dcc21")
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "a b"]) == nil)
#expect(ErrorReporting.releaseFromEnvironment(["FN_RELEASE": "x\"}"]) == nil)
#expect(
ErrorReporting.releaseFromEnvironment(["FN_RELEASE": String(repeating: "a", count: 65)])
== nil)
let event = ReportEvent(level: .error, message: "m")
let with = String(
decoding: try SentryEnvelope.encode(event, environment: "p", release: "98dcc21"),
as: UTF8.self)
#expect(with.contains(#""release":"98dcc21""#))
let without = String(
decoding: try SentryEnvelope.encode(event, environment: "p"), as: UTF8.self)
#expect(!without.contains("release"))
}
@Test("a reporter built with a release stamps it on what it sends")
func reporterSendsRelease() async throws {
let transport = RecordingTransport()
let reporter = ErrorReporter(
dsn: Self.dsn, environment: "test", release: "98dcc21", transport: transport)
#expect(reporter.capture(ReportEvent(level: .error, message: "boom")))
try await transport.waitFor(1)
#expect(transport.bodies.first?.contains(#""release":"98dcc21""#) == true)
}
// MARK: - Helpers
/// The event without its random id, timestamp and hash, which could
@@ -367,6 +432,13 @@ struct ErrorReportingTests {
app.get("unavailable") { _ -> String in
throw Abort(.serviceUnavailable)
}
app.get("unavailable-with-error") { req -> String in
req.logger.error("redis went away")
throw Abort(.serviceUnavailable)
}
app.get("bad-gateway") { _ -> String in
throw Abort(.badGateway)
}
do { try await body(app) } catch {
try? await app.asyncShutdown()
throw error
+21 -4
View File
@@ -87,7 +87,8 @@ store keeps its own copy of each.
| `FN_HOST` | `127.0.0.1` | Not reachable from outside the host as shipped. Terminate TLS in front of it. |
| `FN_PORT` | `8080` | |
| `FN_LOG_LEVEL` | `error` | Never raise to `debug` on a machine with real accounts: it logs request URLs, and a protocol URL contains a device id. |
| `FN_SENTRY_DSN` | unset | Error reporting to a Sentry-protocol collector you run (GlitchTip), as `http://<key>@<host>:<port>/<project>`. Unset sends nothing. Set, errors, criticals and 5xx responses each become one event: the message with account ids, device ids, addresses, tokens, emails and phone numbers removed, the error type, the route template (`/e2e/devices/:deviceID`, never the path asked for), method, status and the source line. No headers, bodies, query strings, addresses or logger metadata. Warnings stay on stdout; info and debug are never sent. At most 30 events a minute, one per fault per 60 seconds, 2 second timeout, no retries. The collector keeps what it receives for as long as it is configured to, so point it at one whose retention you have chosen. A value that is not a DSN stops the relay at start-up. |
| `FN_SENTRY_DSN` | unset | Error reporting to a Sentry-protocol collector you run (GlitchTip), as `http://<key>@<host>:<port>/<project>`. Unset sends nothing. Set, errors, criticals and 5xx responses other than 503 (what a draining relay answers during a restart) each become one event: the message with account ids, device ids, addresses, tokens, emails and phone numbers removed, the error type, the route template (`/e2e/devices/:deviceID`, never the path asked for), method, status and the source line. No headers, bodies, query strings, addresses or logger metadata. Warnings stay on stdout; info and debug are never sent. At most 30 events a minute, one per fault per 60 seconds, 2 second timeout, no retries. The collector keeps what it receives for as long as it is configured to, so point it at one whose retention you have chosen. A value that is not a DSN stops the relay at start-up. |
| `FN_RELEASE` | unset | The release error reports are tagged with. Baked into the image at build time, normally the commit sha: `docker build --build-arg FN_RELEASE=$(git rev-parse --short HEAD)`, or `FN_RELEASE=<sha> Scripts/staging-gate.sh`, which passes it on. Letters, digits and `._-+`, at most 64 characters; anything else, or unset, and reports carry no release. Read only when `FN_SENTRY_DSN` is set. |
**HTTPS is not optional in practice.** Clients hold key material, and serving
them over plain HTTP hands it to anyone on the path. Forward the client address
@@ -115,9 +116,25 @@ operators you do not run. That is the honest reason this is off by default. If
your relay exists so one group can talk without anyone else involved, `closed`
is the correct answer, not the cautious one.
A closed relay opens no outbound connections at all. The only outbound
code in the relay is the SMTP client below, itself disabled unless configured.
No telemetry, no update check, no directory to register with.
The relay opens an outbound connection only for a feature you have switched
on. Each one, and the setting that turns it on:
- Password-reset mail, over SMTP to `FN_SMTP_HOST`.
- Error reports, to the collector named in `FN_SENTRY_DSN`. Off unless set.
It sends only to that address, and only scrubbed errors and 5xx responses
other than 503.
- iOS wake-ups, to Apple's push service, when `FN_APNS_KEY_P8`,
`FN_APNS_KEY_ID` and `FN_APNS_TEAM_ID` are all set.
- Federation, to peer relays, when `FN_FEDERATION` is `allowlist` or `open`.
Sending to a peer also needs `FN_FEDERATION_KEY`.
- Donations, to `api.stripe.com`, when `FN_DONATE_SECRET` is set.
- Attachments, to an S3-compatible store, when `FN_S3_ENDPOINT` is set.
It also connects to the databases you point it at: Postgres at
`FN_POSTGRES_URL` and Redis at `FN_REDIS_URL`. Nothing reports to this
project: there is no telemetry, no update check and no directory to register
with. With every setting above unset, the relay keeps its data in a local
SQLite file and opens no outbound connections.
## Shared FlameNet accounts
+19 -3
View File
@@ -128,9 +128,25 @@ FN_FEDERATION_PEERS=relay.friend.example
FN_FEDERATION_KEY=<base64 Ed25519 seed>
```
A closed relay opens no outbound connections at all. No telemetry, no
update check, no directory to register with. The only outbound code is the SMTP
client, itself disabled unless configured.
The relay opens an outbound connection only for a feature you have switched
on. Each one, and the setting that turns it on:
- Password-reset mail, over SMTP to `FN_SMTP_HOST`.
- Error reports, to the collector named in `FN_SENTRY_DSN`. Off unless set.
It sends only to that address, and only scrubbed errors and 5xx responses
other than 503.
- iOS wake-ups, to Apple's push service, when `FN_APNS_KEY_P8`,
`FN_APNS_KEY_ID` and `FN_APNS_TEAM_ID` are all set.
- Federation, to peer relays, when `FN_FEDERATION` is `allowlist` or `open`.
Sending to a peer also needs `FN_FEDERATION_KEY`.
- Donations, to `api.stripe.com`, when `FN_DONATE_SECRET` is set.
- Attachments, to an S3-compatible store, when `FN_S3_ENDPOINT` is set.
It also connects to the databases you point it at: Postgres at
`FN_POSTGRES_URL` and Redis at `FN_REDIS_URL`. Nothing reports to this
project: there is no telemetry, no update check and no directory to register
with. With every setting above unset, the relay keeps its data in a local
SQLite file and opens no outbound connections.
**Federation costs metadata.** Each relay in an exchange learns the other had
traffic for it, so a slice of your users' social graph is spread across