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> |
||
|---|---|---|
| dist | ||
| examples | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| README.md | ||
Pollux - A Simple Gemini Server
Pollux is a lightweight Gemini server for serving static files securely. It supports virtual hosting, allowing multiple Gemini capsules on a single server instance. Features include TLS encryption, hostname validation, directory serving, and comprehensive security protections.
Requirements
Rust 1.70+ and Cargo.
Building
Clone or download the source, then run:
cargo build --release
This produces the target/release/pollux binary.
Virtual Hosting
Pollux supports virtual hosting, allowing you to serve multiple Gemini capsules from a single server. Each hostname can have its own root directory, certificates, and configuration.
Configuration
Create a config file at /etc/pollux/config.toml or use --config to specify a path:
# Global settings (optional)
bind_host = "0.0.0.0"
port = 1965
max_concurrent_requests = 1000 # requests processed at once (default 1000)
max_connections = 512 # connections handled at once (default 512)
# Virtual host configurations
["example.com"]
root = "/var/gemini/example.com"
cert = "/etc/ssl/example.com.crt"
key = "/etc/ssl/example.com.key"
["blog.example.com"]
root = "/var/gemini/blog"
cert = "/etc/ssl/blog.crt"
key = "/etc/ssl/blog.key"
["another-site.net"]
root = "/var/gemini/another"
cert = "/etc/ssl/another.crt"
key = "/etc/ssl/another.key"
Set the log level with the RUST_LOG environment variable (for example
RUST_LOG=info) rather than in the config file.
Keep max_connections comfortably below the process file-descriptor limit
(ulimit -n, or LimitNOFILE in the systemd unit). Connections above the
limit are dropped immediately, which keeps the listener responsive.
Not implemented: per-host
portandlog_levelkeys are still parsed for backwards compatibility but have no effect — every host is served on the single global port. Pollux logs a warning at startup if it sees them.
Features
- Multiple hostnames on a single server instance
- Per-host TLS certificates selected by SNI, so each capsule presents its own identity
- Automatic content isolation - each host serves only its own files
- Path security - directory traversal and symlink escapes are blocked
- Hidden files are private - dotfiles and dot-directories (
.git,.env) are never served - Streamed responses - memory use is independent of file size, capped at 64 MiB per response
- Index file serving -
index.gmifiles are served automatically - Hostname validation - DNS-compliant hostname checking, matched case-insensitively
Request Routing
gemini://example.com/→ serves/var/gemini/example.com/index.gmigemini://blog.example.com/article.gmi→ serves/var/gemini/blog/article.gmigemini://unknown.com/→ returns status 53 "Proxy request refused"
Single Host Mode (Legacy)
For backward compatibility, you can still use the old single-host format:
root = "/path/to/static/files"
cert = "/path/to/cert.pem"
key = "/path/to/key.pem"
hostname = "gemini.example.com"
bind_host = "0.0.0.0"
port = 1965
max_concurrent_requests = 1000
Development Setup
Quick Start with Self-Signed Certs
mkdir -p tmp
openssl req -x509 -newkey rsa:2048 \
-keyout tmp/key.pem \
-out tmp/cert.pem \
-days 365 \
-nodes \
-subj "/CN=localhost"
Update config.toml:
cert = "tmp/cert.pem"
key = "tmp/key.pem"
Run the server:
./pollux --config /path/to/config.toml
Access with a Gemini client like Lagrange at gemini://yourdomain.com/.
Development Notes
- These certificates are for local testing only
- Browsers will show security warnings with self-signed certs
- Certificates in the
dev/directory are gitignored for security
Options
--config(-C): Path to config file (default/etc/pollux/config.toml)--test-processing-delay(debug builds only): Add delay before processing requests (seconds) - for testing rate limiting
Logging
Pollux uses the tracing crate for structured logging. Configure log levels with the RUST_LOG environment variable:
# Basic usage
export RUST_LOG=info
./pollux
# Module-specific levels
export RUST_LOG=pollux=debug,sqlx=info
# Maximum verbosity
export RUST_LOG=trace
Available levels: error, warn, info, debug, trace
Security
Pollux is designed with security as a priority:
- Path traversal protection - requests like
../../../etc/passwdare blocked - TLS encryption - all connections are encrypted with valid certificates
- Content isolation - virtual hosts cannot access each other's files
- Request validation - malformed requests are rejected
- Rate limiting - configurable concurrent request limits prevent abuse
Best Practices
Virtual Hosting Setup
- Use separate TLS certificates for each hostname when possible
- Keep host root directories separate and properly permissioned
- Use DNS-compliant hostnames (no underscores, proper formatting)
- Monitor logs for unknown hostname attempts
Certificate Management
- Never commit certificate files to version control
- Use development certificates only for local testing
- Production certificates should be obtained via Let's Encrypt or your CA
- Rotate certificates regularly and restart the server
File Organization
- Create
index.gmifiles in directories for automatic serving - Use
.gmiextension for Gemini text files - Store certificates outside web-accessible directories
- Use proper file permissions (readable by server user only)
Limitations
- No dynamic content - Pollux serves only static files
- Single certificate per server - All hosts currently share the same TLS certificate (can be enhanced)
- No CGI support - No server-side processing or scripting
- Memory usage - All host configurations are loaded into memory
- No HTTP support - Gemini protocol only
Examples
The examples/ directory contains sample configuration files:
virtual-hosting.toml- Multi-host setup with different certificatessingle-host.toml- Legacy single-host configurationdevelopment.toml- Local development with self-signed certificates
Testing
Run cargo test for the full test suite, which includes comprehensive integration tests covering:
- Virtual hosting with multiple hostnames
- TLS certificate validation
- Path security and isolation
- Concurrent request handling
- Performance validation
Note: Some integration tests use Python 3 for Gemini protocol validation. If Python 3 is not available, certain tests will be skipped automatically.