6.0 KiB
Simplex.FileTransfer.Server
XFTP router: HTTP/2 request handling, handshake state machine, data packet operations, and statistics.
Source: FileTransfer/Server.hs
Architecture
The XFTP router runs several concurrent threads via raceAny_:
| Thread | Purpose |
|---|---|
runServer |
HTTP/2 router accepting data packet transfer requests |
expireFiles |
Periodic data packet expiration with throttling |
logServerStats |
Periodic stats flush to CSV |
savePrometheusMetrics |
Periodic Prometheus metrics dump |
runCPServer |
Control port for admin commands |
See spec/routers.md for component and sequence diagrams.
Non-obvious behavior
1. Three-state handshake with session caching
The router maintains a TMap SessionId Handshake with three states:
- No entry: first request — for non-SNI or
xftp-web-hellorequests,processHellogenerates DH key pair and sends router handshake; for SNI requests withoutxftp-web-hello, returnsSESSIONerror HandshakeSent pk: router hello sent, waiting for client handshake with version negotiationHandshakeAccepted thParams: handshake complete, subsequent requests use cached params
Web clients can re-send hello (xftp-web-hello header) even in HandshakeSent or HandshakeAccepted states — the router reuses the existing private key rather than generating a new one.
2. Web identity proof via challenge-response
When a web client sends a hello with a non-empty body, the router parses an XFTPClientHello containing a webChallenge. The router signs challenge <> sessionId with its long-term key and includes the signature in the handshake result. This proves router identity to web clients that cannot verify TLS certificates directly.
3. skipCommitted drains request body on re-upload
If receiveServerFile detects the data packet is already uploaded (filePath TVar is Just), it cannot simply ignore the request body — the HTTP/2 client would block waiting for the router to consume it. Instead, skipCommitted reads and discards the entire body in fileBlockSize increments, returning FROk when complete. This makes FPUT idempotent from the client's perspective.
4. Atomic quota reservation with rollback
receiveServerFile uses stateTVar to atomically check and reserve storage quota before receiving the data packet. If the upload fails (timeout, size mismatch, IO error), the reserved size is subtracted from usedStorage and the partial data packet is deleted on the router. This prevents failed uploads from permanently consuming quota.
5. retryAdd generates new IDs on collision
createFile and addRecipient use retryAdd which generates a random ID and makes up to 3 total attempts (initial + 2 retries) on DUPLICATE_ errors. This handles the astronomically unlikely case of random ID collision without requiring uniqueness checking before insertion.
6. Timing attack mitigation on entity lookup
verifyXFTPTransmission calls dummyVerifyCmd (imported from SMP router) when a data packet entity is not found. This equalizes result timing to prevent attackers from distinguishing "entity doesn't exist" from "signature invalid" based on latency.
7. BLOCKED vs EntityOff distinction
When verifyXFTPTransmission reads fileStatus:
EntityActive→ proceed with commandEntityBlocked info→ returnBLOCKEDwith blocking reasonEntityOff→ returnAUTH(same as entity-not-found)
EntityOff is treated identically to missing entities for information-hiding purposes.
8. blockServerFile deletes the stored data packet
Despite the name suggesting it only marks a data packet as blocked, blockServerFile also deletes the stored data packet from disk via deleteOrBlockServerFile_. The deleted = True parameter to blockFile in the store adjusts usedStorage. A blocked data packet returns BLOCKED errors on access but has no data on disk.
9. Stats restore overrides counts from live store
restoreServerStats loads stats from the backup file but overrides _filesCount and _filesSize with values computed from the live file store (TMap size and usedStorage TVar). If the backup values differ, warnings are logged. This handles cases where data packets were expired or deleted while the router was down.
10. Data packet expiration with configurable throttling
expireServerFiles accepts an optional itemDelay (100ms when called from the periodic thread, Nothing at router startup). Between each data packet check, threadDelay itemDelay prevents expiration from monopolizing IO. At startup, data packets are expired without delay to clean up quickly.
11. Stats log aligns to wall-clock midnight
logServerStats computes an initialDelay to align the first stats flush to logStatsStartTime (default 0 = midnight UTC). If the target time already passed today, it adds 86400 seconds for the next day. Subsequent flushes use exact logInterval cadence.
12. Stored data packet deleted before store cleanup
deleteOrBlockServerFile_ removes the stored data packet first, then runs the STM store action. If the process crashes between these two operations, the store will reference a data packet that no longer exists on disk. The next access would return AUTH (data packet not found on disk), and eventual expiration would clean the store entry.
13. SNI-dependent CORS and web serving
CORS headers require both sniUsed = True and addCORSHeaders = True in the transport config. Static web page serving is enabled when sniUsed = True. Non-SNI connections (direct TLS without hostname) skip both CORS and web serving. This separates the web-facing and protocol-facing behaviors of the same router port.
14. Control port data packet operations use recipient index
CPDelete and CPBlock commands look up data packets via getFile fs SFRecipient fileId, meaning the control port takes a recipient ID, not a sender ID. This is the ID visible to recipients and contained in data packet descriptions.