clientIP() now checks the TCP peer first. When the peer is in FN_TRUSTED_PROXY_ADDRS (addresses or CIDR ranges, IPv4 or IPv6), it reads the header as before, FN_TRUSTED_PROXIES hops from the right. From any other peer it ignores the header and returns the peer's own address. Unset trusts nobody, so a relay exposed directly cannot be handed a false address. Behind a proxy it meters everyone as the proxy until the variable is set. An entry that does not parse matches nothing. On our box the relay shares the web network with 43 other containers, each able to connect past Traefik, geogate and CrowdSec. Production needs Traefik's web address pinned and listed here before this deploys. The onion path is unchanged: gateway -> Traefik -> relay, peer Traefik. CONFIGURATION, HOSTING, SECURITY and CONTRIBUTING say so; the generated pages are rebuilt; docker-compose.prod.yml passes the variable through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.8 KiB
Contributing
This is the Flamenet relay: a Swift/Vapor server that stores public keys and opaque ciphertext, and holds no key that opens a message. Patches welcome.
Security bugs do not go in an issue
Email security@flamenetmessenger.com. See SECURITY.md for what this server is and is not trusted with, that section is the fastest way to tell whether something you found is a bug or the design.
How this repository relates to the others
It is standalone, and patches land here. The relay once lived as
server/ inside the iOS app's tree and this repository was an rsync extract of
it, so a commit made here was overwritten by the next refresh. That is over,
the split carried the history with it. If you find a comment claiming this is a
mirror, it is stale and worth a patch of its own.
What still spans repositories is the conformance harness: the protocol and its §8 test suite live in flamenet-e2e, and CI clones it at HEAD rather than vendoring a copy. That coupling is deliberate, the harness carries the spec, and a relay tested against a pinned old harness is tested against a pinned old spec.
Issues and merge requests
Work is tracked in GitLab issues, one per thing, in the repository where the fix lands. Four repositories carry this project (see above), so an issue about the apps does not belong here just because the relay noticed it.
Cross-project references work and should be used: paulhitt/flamenet-e2e#1
renders as a link from any of the four. Use them rather than restating the other
issue, so the two never drift.
A branch is named <issue>-<slug>, 1-metadata-sealed-rosters. The merge
request opens with Closes #N on its own line, which links the two and shuts
the issue on merge. Without it the issue stays open after the fix ships and
somebody re-does the work.
Closes #N is resolved in the repository the branch is pushed to. Getting
the repository wrong does not error, it silently points at whatever issue holds
that number there, or at nothing. Check git remote get-url origin before you
commit, not after you push.
Labels
| label | means |
|---|---|
accuracy |
A public page or doc claims something the code does not do |
privacy |
Changes what is stored, or what we say is stored |
parity |
One platform has it and the other does not |
unwired |
Built in the engine, not reachable from the UI |
ops |
Deploy, CI, infrastructure |
blocked:apple |
Waiting on App Store review |
accuracy outranks the rest. A page that overstates what we protect is worse
than a missing feature: somebody reads it and decides what to say on this
network. Fix those first, and fix them in the direction of claiming less.
unwired is not a nice-to-have. Engine work that no screen reaches
protects nobody, and it is the failure this project keeps repeating, sealed
rosters, group names and delivery capabilities all landed in the engine well
before any UI called them. An issue that only closes half of that is not closed.
A doc change that crosses repositories
Several pages here are generated from markdown that lives in flamenet-e2e
(/spec, /self-hosting, /audit, /security and others). Scripts/check-doc-pages.py
compares each page against its source as it stands on flamenet-e2e main,
which it clones when the job runs.
That means a cross-repo doc change makes this repository's main red for a
while, and the guard is right to say so. A regenerated page can only match the
new hash after the source is on e2e main, so between "source lands" and
"regeneration lands" the served page is stale. Sequence that works:
- Merge the source change in
flamenet-e2e. - Immediately run
Scripts/build-doc-pages.shhere and merge the result. - Deploy, because the pages ship inside the relay image.
Do not shorten step 2 by making the job allow_failure or skipping it on
main. A stale /self-hosting was served for a day telling people to run a
command that did not work; the guard exists because of that, and a green that
means nothing is how it comes back.
The same failure has a second cause, and the fix is the opposite one. If
your branch was cut before somebody else's regeneration landed, it carries the
old pages while the source has moved, identical message, but running the
generator produces no diff and leaves you stuck. Merge main in instead. Tell
the two apart by asking whether main already has a newer page than your branch:
git fetch && git log --oneline HEAD..origin/main -- Resources/Views/
Anything listed there means it is staleness against main, not against the
source.
A slow queue makes this window long rather than momentary, the first time it
happened, a two-minute gap became most of an hour. See the issues labelled ops.
What an issue should carry
The evidence, not the impression. A path and line, a query and its output, a
grep and what it printed. "Groups may still be plaintext" ages badly; 4 plaintext, 0 sealed does not, and tells the next person how to re-check it.
Say what "done" is as a checklist, and include the verification step, the thing somebody runs to prove it, not just the thing they change.
Building and testing
Swift 6.1: pinned, and pinned to match Dockerfile. Not 6.0: postgres-kit
2.16.1 declares swift-tools-version 6.1 and a 6.0 toolchain refuses to resolve
it. If you move one, move the other, or CI passes and the shipped image does not
build.
swift build
swift test
swift test prints "Executed 0 tests" and that is not an empty suite.
These are swift-testing tests; the XCTest half is empty and the real
results appear separately as Test run with N tests ... passed. This has been
misread as a broken suite before, which is why CI greps for that line and fails
the job without it.
For the HTTP-level suite, §8 conformance, driven over real sockets by the
protocol's own harness, clone flamenet-e2e and point it back here:
git clone --depth 1 https://gitlab.com/extant2000/flamenet-e2e.git /tmp/e2e
cd /tmp/e2e && FN_RELAY_DIR=/path/to/flamenet-relay node test/relay.mjs
That harness is deliberately coupled to flamenet-e2e at HEAD: it carries the
spec, and a relay tested against a pinned old harness is tested against a pinned
old spec.
Configuration is documented in .env.example, every variable the relay reads
belongs there, and CI fails if the file is missing.
What a change needs
- Touching a route? Add a case to the conformance harness in
flamenet-e2e. The unit suite does not speak HTTP;relay.mjsis what makes §8 a gate rather than a claim. - Touching a database schema? A new migration, never an edit to an applied one. Appending DDL to a migration that already ran is a silent no-op in production.
- Touching anything on the boot path? Vapor services, the Redis pool above
all, fatal-error if touched before
didBoot. That mistake crash-looped production for real. New startup work goes in the existingBootTasklifecycle handler, not at file scope and not inconfigure. - Touching rate limiting? Per-IP limits read
X-Forwarded-Foronly when the TCP peer is inFN_TRUSTED_PROXY_ADDRS, and count from the right, bounded byFN_TRUSTED_PROXIES. Counting from the left, or believing any peer, lets a client choose its own bucket. Sign-in and quota limiters fail closed to a local fallback when Redis is unreachable; keep it that way. - Adding a claim to a public page or to
/metadata? It has to be true of the running configuration, not of the code in the abstract./metadatarenders from the instance's own config for exactly that reason.
House rules
- Fail closed. Unparseable input, unknown versions and absent rows are refusals. An absent device row means retired, not fine.
- Never log ciphertext, tokens, or key material. Refusals are logged; contents are not.
- No secrets in the repository.
FN_ISSUER_PRIVATE_KEYandFN_AT_REST_SECRETlive in the environment; a leaked issuer key mints tokens for any account. - Comments explain which attack a line prevents. That is the highest-value comment in this codebase and there are a lot of them already, match it.
Deploying
Not from CI. The image is built here and never pushed; publishing a relay image is a deployment decision, not a pipeline side effect. The project's own production deploys go through a staging gate that boots a candidate stack with Redis, smoke-tests it, holds a stability window, and retags the previous image before switching. If you self-host, do something equivalent, and rehearse the database restore, because a backup nobody has restored is a hypothesis.
Comments and prose
Plain text. No emoji or symbol markers, no markdown bold inside code comments, no em-dashes. Say what the code does and why, briefly; the history of how it got that way belongs in the commit message, not in the file.