Files
simplexmq/spec/modules/Simplex/FileTransfer/Server.md
Evgeny @ SimpleX Chat a7c6dde39f router diagrams
2026-03-14 09:50:45 +00:00

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-hello requests, processHello generates DH key pair and sends router handshake; for SNI requests without xftp-web-hello, returns SESSION error
  • HandshakeSent pk: router hello sent, waiting for client handshake with version negotiation
  • HandshakeAccepted 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 command
  • EntityBlocked info → return BLOCKED with blocking reason
  • EntityOff → return AUTH (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.