An adversarial review of the request path turned up nine issues, all reproduced against a running server before and after the fix. The most serious was a permanent denial of service. The in-flight request counter was decremented on paths that never incremented it; being unsigned it wrapped to usize::MAX, after which every request was answered with "41 Server unavailable" until the process restarted. A client that completed the TLS handshake and disconnected without sending a request was enough -- a port scan, a health check, a cancelled page load. The counter is replaced by a semaphore permit released on every exit path. Two further remote denials of service: an error from accept() propagated out of main and ended the process, so exhausting the descriptor limit killed the server; and responses were read whole into memory, so cost scaled with concurrent requests times file size. Accept errors are now logged and retried after a backoff, and responses stream in 64 KiB chunks capped at 64 MiB. Peers that open a socket and never send a ClientHello are bounded by a 10s handshake timeout and a connection cap (max_connections, default 512). Every virtual host was served whichever certificate came first in a HashMap, which varies per process, so a host's certificate changed between restarts. Because Gemini clients pin certificates on first use, this trained users to dismiss the mismatch warning that would otherwise reveal interception. Certificates are now selected by SNI with a deterministic fallback, so unknown hosts still complete a handshake and receive 53. Dotfiles inside a content root were public, exposing .git/config and any credentials in it for capsule roots that are git working copies. Path resolution is now in-tree: reject non-plain components, then compare canonical prefixes. This replaces path-security, an unaudited micro-crate in the security boundary that also rejected legitimate filenames containing '%', '~' or '$'. rustls moves 0.21 -> 0.23; the 0.21 branch is end of life. TLS 1.2 and 1.3 only, as before. Malformed requests logged attacker-controlled text at ERROR on every request, letting a client drive log volume; client-caused conditions now log at debug, truncated. Hostname routing accepts the authority forms the specification permits (mixed case, explicit port, userinfo, trailing dot), which previously returned 53. Per-host port and log_level were accepted and silently ignored, and now warn at startup. Adds tests/connection_lifecycle.rs covering the counter underflow, the oversized-response refusal, hidden files and authority normalization. Fixes the test client, which sent host:port as SNI. 42 -> 56 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
No EOL
4.8 KiB
Markdown
87 lines
No EOL
4.8 KiB
Markdown
# Changelog
|
|
|
|
All notable changes to Pollux will be documented in this file.
|
|
|
|
## [Unreleased]
|
|
|
|
### Security
|
|
|
|
- **Fixed a permanent denial of service from a single aborted connection**: the
|
|
in-flight request counter was decremented on paths that never incremented it.
|
|
Being unsigned it wrapped to `usize::MAX`, after which every request was
|
|
refused with `41 Server unavailable` until the process was restarted. A client
|
|
that completed the TLS handshake and disconnected without sending a request —
|
|
a port scan, a health check, a cancelled page load — was enough. The counter is
|
|
replaced by a semaphore permit that is released on every exit path.
|
|
- **Fixed a remote crash through file-descriptor exhaustion**: an error from
|
|
`accept()` propagated out of `main` and ended the process. Accept errors are
|
|
now logged and retried after a short backoff, so the listener survives.
|
|
- **Added a TLS handshake timeout (10s) and a connection limit**
|
|
(`max_connections`, default 512): a peer that opened a socket and never sent a
|
|
ClientHello previously held a task and descriptor indefinitely.
|
|
- **Fixed unbounded memory use when serving files**: responses were read whole
|
|
into memory, so cost scaled with concurrent requests times file size (eight
|
|
requests for a 144 MB file reached 1.18 GB resident). Responses now stream in
|
|
64 KiB chunks and are capped at 64 MiB, refused with `50` above that.
|
|
- **Fixed nondeterministic TLS certificate selection**: all virtual hosts were
|
|
served whichever certificate came first in a `HashMap`, which varies per
|
|
process, so a host's certificate changed between restarts. Because Gemini
|
|
clients pin certificates on first use, this trained users to dismiss the
|
|
mismatch warning that would otherwise reveal interception. Certificates are
|
|
now selected by SNI, with a deterministic fallback.
|
|
- **Stopped serving hidden files**: dotfiles and dot-directories inside a content
|
|
root were public, exposing `.git/config` and any credentials in it for capsule
|
|
roots that are git working copies.
|
|
- **Replaced the `path-security` dependency** with an in-tree check (reject
|
|
non-plain path components, then compare canonical prefixes). Same guarantees
|
|
against traversal and symlink escape, no third-party code in the security
|
|
boundary, and no false positives on filenames containing `~`, `$` or `%`.
|
|
- **Upgraded rustls 0.21 → 0.23**; the 0.21 branch is end-of-life and no longer
|
|
receives security fixes. TLS 1.2 and 1.3 only, as before.
|
|
- **Bounded and downgraded request logging**: malformed requests logged
|
|
attacker-controlled text at `ERROR` on every request, letting a client drive
|
|
log volume. Client-caused conditions now log at `debug` and logged request
|
|
text is truncated.
|
|
|
|
### Fixed
|
|
|
|
- Hostname routing now accepts the authority forms the Gemini specification
|
|
permits: mixed case, an explicit port, userinfo, and a fully-qualified trailing
|
|
dot. These previously returned `53 Proxy request refused`.
|
|
- File extensions are matched case-insensitively, so `PHOTO.JPEG` is served as
|
|
`image/jpeg` rather than `application/octet-stream`.
|
|
- Per-host `port` and `log_level` were accepted and silently ignored; Pollux now
|
|
warns at startup that they have no effect.
|
|
- Response reads and writes have a 60s timeout, so a client that stops reading
|
|
cannot hold a concurrency permit open indefinitely.
|
|
|
|
### Removed
|
|
|
|
- Dead `src/logging.rs` stub, the unused `parse_gemini_url` duplicate URL parser,
|
|
and the unused `time` dependency.
|
|
|
|
## [1.0.0] - 2026-01-17
|
|
|
|
### Added
|
|
- **Complete Gemini Server Implementation**: Full-featured Gemini protocol server
|
|
- **Rate Limiting**: Configurable concurrent request limiting with proper 41 status responses
|
|
- **Comprehensive Config Validation**: Graceful error handling for all configuration issues
|
|
- **Configurable Logging**: Custom log format with timestamp, level, IP, request, and status
|
|
- **Dual Host Configuration**: Separate bind_host (interface) and hostname (validation) settings
|
|
- **Integration Tests**: Full test suite including config validation and rate limiting
|
|
- **Systemd Integration**: Complete service file and installation documentation
|
|
- **Security Features**: Path traversal protection, request size limits, URI validation
|
|
- **TLS Support**: Full certificate handling with manual certificate setup
|
|
|
|
### Security
|
|
- **Path Traversal Protection**: Prevent access outside configured root directory
|
|
- **Request Size Limits**: Reject requests over 1026 bytes (per Gemini spec)
|
|
- **URI Validation**: Strict Gemini URL format checking and hostname validation
|
|
- **Certificate Security**: Proper private key permission handling
|
|
|
|
### Development
|
|
- **Test Infrastructure**: Comprehensive integration and unit test suite (22 tests)
|
|
- **Code Quality**: Clippy clean with zero warnings
|
|
- **Documentation**: Complete installation and configuration guides
|
|
- **CI/CD Ready**: Automated testing and building</content>
|
|
<parameter name="filePath">CHANGELOG.md |