Implement dual host configuration: bind_host and hostname

- Replace 'host' config with separate 'bind_host' and 'hostname'
- bind_host: IP/interface for server binding (default 0.0.0.0)
- hostname: Domain for URI validation (required)
- Update all parsing and validation code
- Create dist/ directory with systemd service, config, and install guide
- Add comprehensive INSTALL.md with setup instructions
This commit is contained in:
Jeena 2026-01-16 12:46:27 +00:00
parent 1665df65da
commit ea8083fe1f
7 changed files with 333 additions and 13 deletions

223
dist/INSTALL.md vendored Normal file
View file

@ -0,0 +1,223 @@
# Installing Pollux Gemini Server
This guide covers installing and configuring the Pollux Gemini server for production use.
## Prerequisites
- Linux system with systemd
- Rust toolchain (for building from source)
- Domain name with DNS configured
- Let's Encrypt account (for certificates)
## Quick Start
```bash
# 1. Build and install
cargo build --release
sudo cp target/release/pollux /usr/local/bin/
# 2. Get certificates
sudo certbot certonly --standalone -d example.com
# 3. Create directories and user
sudo useradd -r -s /bin/false gemini
sudo usermod -a -G ssl-cert gemini
sudo mkdir -p /etc/pollux /var/www/example.com
sudo chown -R gemini:gemini /var/www/example.com
# 4. Install config
sudo cp dist/config.toml /etc/pollux/
# 5. Add your Gemini content
sudo cp -r your-content/* /var/www/example.com/
# 6. Install and start service
sudo cp dist/pollux.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable pollux
sudo systemctl start pollux
# 7. Check status
sudo systemctl status pollux
sudo journalctl -u pollux -f
```
## Detailed Installation
### Building from Source
```bash
git clone https://github.com/yourusername/pollux.git
cd pollux
cargo build --release
sudo cp target/release/pollux /usr/local/bin/
```
### Certificate Setup
#### Let's Encrypt (Recommended)
```bash
# Install certbot
sudo apt install certbot # Ubuntu/Debian
# OR
sudo dnf install certbot # Fedora/RHEL
# Get certificate
sudo certbot certonly --standalone -d example.com
# Verify permissions
ls -la /etc/letsencrypt/live/example.com/
# Should show fullchain.pem and privkey.pem
```
#### Self-Signed (Development Only)
```bash
# Generate certificates
openssl req -x509 -newkey rsa:4096 \
-keyout /etc/pollux/key.pem \
-out /etc/pollux/cert.pem \
-days 365 -nodes \
-subj "/CN=example.com"
# Set permissions
sudo chown gemini:gemini /etc/pollux/*.pem
sudo chmod 644 /etc/pollux/cert.pem
sudo chmod 600 /etc/pollux/key.pem
```
### User and Directory Setup
```bash
# Create service user
sudo useradd -r -s /bin/false gemini
# Add to certificate group (varies by distro)
sudo usermod -a -G ssl-cert gemini # Ubuntu/Debian
# OR
sudo usermod -a -G certbot gemini # Some systems
# Create directories
sudo mkdir -p /etc/pollux /var/www/example.com
sudo chown -R gemini:gemini /var/www/example.com
```
### Configuration
Edit `/etc/pollux/config.toml`:
```toml
root = "/var/www/example.com"
cert = "/etc/letsencrypt/live/example.com/fullchain.pem"
key = "/etc/letsencrypt/live/example.com/privkey.pem"
bind_host = "0.0.0.0"
hostname = "example.com"
port = 1965
max_concurrent_requests = 1000
log_level = "info"
```
### Content Setup
```bash
# Copy your Gemini files
sudo cp -r gemini-content/* /var/www/example.com/
# Set permissions
sudo chown -R gemini:gemini /var/www/example.com
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
```
### Service Installation
```bash
# Install service file
sudo cp dist/pollux.service /etc/systemd/system/
# If your paths differ, edit the service file
sudo editor /etc/systemd/system/pollux.service
# Update ReadOnlyPaths to match your config
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable pollux
sudo systemctl start pollux
```
### Verification
```bash
# Check service status
sudo systemctl status pollux
# View logs
sudo journalctl -u pollux -f
# Test connection
openssl s_client -connect example.com:1965 -servername example.com <<< "gemini://example.com/\r\n" | head -1
```
## Troubleshooting
### Permission Issues
```bash
# Check certificate access
sudo -u gemini cat /etc/letsencrypt/live/example.com/fullchain.pem
# Check content access
sudo -u gemini ls -la /var/www/example.com/
```
### Port Issues
```bash
# Check if port is in use
sudo netstat -tlnp | grep :1965
# Test binding
sudo -u gemini /usr/local/bin/pollux # Should show startup messages
```
### Certificate Issues
```bash
# Renew certificates
sudo certbot renew
# Reload service after cert renewal
sudo systemctl reload pollux
```
## Configuration Options
See `config.toml` for all available options. Key settings:
- `root`: Directory containing your .gmi files
- `cert`/`key`: TLS certificate paths
- `bind_host`: IP/interface to bind to
- `hostname`: Domain name for URI validation
- `port`: Listen port (1965 is standard)
- `max_concurrent_requests`: Connection limit
- `log_level`: Logging verbosity
## Upgrading
```bash
# Stop service
sudo systemctl stop pollux
# Install new binary
sudo cp target/release/pollux /usr/local/bin/
# Start service
sudo systemctl start pollux
```
## Security Notes
- Certificates are read-only by the service user
- Content directory is read-only
- No temporary file access
- Systemd security hardening applied
- Private keys have restricted permissions
- URI validation prevents domain confusion attacks

68
dist/config.toml vendored Normal file
View file

@ -0,0 +1,68 @@
# Pollux Gemini Server Configuration
#
# This is an example configuration file for the Pollux Gemini server.
# Copy this file to /etc/pollux/config.toml and customize the values below.
#
# The Gemini protocol is specified in RFC 1436: https://tools.ietf.org/rfc/rfc1436.txt
# Directory containing your Gemini files (.gmi, .txt, images, etc.)
# The server will serve files from this directory and its subdirectories.
# Default index file is 'index.gmi' for directory requests.
#
# IMPORTANT: The server needs READ access to this directory.
# Make sure the service user (gemini) can read all files here.
root = "/var/www/example.com"
# TLS certificate and private key files
# These files are required for TLS encryption (Gemini requires TLS).
#
# For Let's Encrypt certificates (recommended for production):
# cert = "/etc/letsencrypt/live/example.com/fullchain.pem"
# key = "/etc/letsencrypt/live/example.com/privkey.pem"
#
# To obtain Let's Encrypt certs:
# sudo certbot certonly --standalone -d example.com
#
# For development/testing, generate self-signed certs:
# openssl req -x509 -newkey rsa:4096 -keyout /etc/pollux/key.pem -out /etc/pollux/cert.pem -days 365 -nodes -subj "/CN=example.com"
cert = "/etc/letsencrypt/live/example.com/fullchain.pem"
key = "/etc/letsencrypt/live/example.com/privkey.pem"
# Server network configuration
#
# bind_host: IP address or interface to bind the server to
# - "0.0.0.0" = listen on all interfaces (default)
# - "127.0.0.1" = localhost only
# - "::" = IPv6 all interfaces
# - Specific IP = bind to that address only
bind_host = "0.0.0.0"
# hostname: Domain name for URI validation
# - Used to validate incoming gemini:// URIs
# - Clients must use: gemini://yourdomain.com
# - Server validates that requests match this hostname
hostname = "example.com"
# port: TCP port to listen on
# - Default Gemini port is 1965
# - Ports below 1024 require root privileges
# - Choose a different port if 1965 is in use
port = 1965
# Request limiting
#
# max_concurrent_requests: Maximum number of simultaneous connections
# - Prevents server overload and DoS attacks
# - Set to 0 to disable limiting (not recommended)
# - Typical values: 100-10000 depending on server capacity
max_concurrent_requests = 1000
# Logging configuration
#
# log_level: Controls how much information is logged
# - "error": Only errors that prevent normal operation
# - "warn": Errors plus warnings about unusual conditions
# - "info": General operational information (recommended)
# - "debug": Detailed debugging information
# - "trace": Very verbose debugging (use only for troubleshooting)
log_level = "info"

24
dist/pollux.service vendored Normal file
View file

@ -0,0 +1,24 @@
[Unit]
Description=Pollux Gemini Server
After=network.target
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/pollux
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
User=gemini
Group=gemini
NoNewPrivileges=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/etc/pollux /etc/letsencrypt/live/example.com /var/www/example.com
# NOTE: Adjust paths to match your config:
# - /etc/letsencrypt/live/example.com for Let's Encrypt certs
# - /var/www/example.com for your content root
# The server needs read access to config, certificates, and content files
[Install]
WantedBy=multi-user.target

View file

@ -5,7 +5,8 @@ pub struct Config {
pub root: Option<String>, pub root: Option<String>,
pub cert: Option<String>, pub cert: Option<String>,
pub key: Option<String>, pub key: Option<String>,
pub host: Option<String>, pub bind_host: Option<String>,
pub hostname: Option<String>,
pub port: Option<u16>, pub port: Option<u16>,
pub log_level: Option<String>, pub log_level: Option<String>,
pub max_concurrent_requests: Option<usize>, pub max_concurrent_requests: Option<usize>,
@ -31,7 +32,8 @@ mod tests {
root = "/path/to/root" root = "/path/to/root"
cert = "cert.pem" cert = "cert.pem"
key = "key.pem" key = "key.pem"
host = "example.com" bind_host = "0.0.0.0"
hostname = "example.com"
port = 1965 port = 1965
log_level = "info" log_level = "info"
"#; "#;
@ -41,7 +43,8 @@ mod tests {
assert_eq!(config.root, Some("/path/to/root".to_string())); assert_eq!(config.root, Some("/path/to/root".to_string()));
assert_eq!(config.cert, Some("cert.pem".to_string())); assert_eq!(config.cert, Some("cert.pem".to_string()));
assert_eq!(config.key, Some("key.pem".to_string())); assert_eq!(config.key, Some("key.pem".to_string()));
assert_eq!(config.host, Some("example.com".to_string())); assert_eq!(config.bind_host, Some("0.0.0.0".to_string()));
assert_eq!(config.hostname, Some("example.com".to_string()));
assert_eq!(config.port, Some(1965)); assert_eq!(config.port, Some(1965));
assert_eq!(config.log_level, Some("info".to_string())); assert_eq!(config.log_level, Some("info".to_string()));
assert_eq!(config.max_concurrent_requests, None); // Default assert_eq!(config.max_concurrent_requests, None); // Default

View file

@ -52,7 +52,8 @@ async fn main() {
root: None, root: None,
cert: None, cert: None,
key: None, key: None,
host: None, bind_host: None,
hostname: None,
port: None, port: None,
log_level: None, log_level: None,
max_concurrent_requests: None, max_concurrent_requests: None,
@ -66,7 +67,8 @@ async fn main() {
let root = config.root.expect("root is required"); let root = config.root.expect("root is required");
let cert_path = config.cert.expect("cert is required"); let cert_path = config.cert.expect("cert is required");
let key_path = config.key.expect("key is required"); let key_path = config.key.expect("key is required");
let host = config.host.unwrap_or_else(|| "0.0.0.0".to_string()); let bind_host = config.bind_host.unwrap_or_else(|| "0.0.0.0".to_string());
let hostname = config.hostname.expect("hostname is required");
let port = config.port.unwrap_or(1965); let port = config.port.unwrap_or(1965);
// Validate max concurrent requests // Validate max concurrent requests
@ -104,22 +106,22 @@ async fn main() {
let acceptor = TlsAcceptor::from(Arc::new(config)); let acceptor = TlsAcceptor::from(Arc::new(config));
let listener = TcpListener::bind(format!("{}:{}", host, port)).await.unwrap(); let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await.unwrap();
// Print startup information // Print startup information
print_startup_info(&host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests); print_startup_info(&bind_host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests);
loop { loop {
let (stream, _) = listener.accept().await.unwrap(); let (stream, _) = listener.accept().await.unwrap();
tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap())); tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap()));
let acceptor = acceptor.clone(); let acceptor = acceptor.clone();
let dir = root.clone(); let dir = root.clone();
let expected_host = "localhost".to_string(); // Override for testing let expected_hostname = hostname.clone(); // Use configured hostname
let max_concurrent = max_concurrent_requests; let max_concurrent = max_concurrent_requests;
let test_delay = test_processing_delay; let test_delay = test_processing_delay;
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(stream) = acceptor.accept(stream).await { if let Ok(stream) = acceptor.accept(stream).await {
if let Err(e) = server::handle_connection(stream, &dir, &expected_host, port, max_concurrent, test_delay).await { if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await {
tracing::error!("Error handling connection: {}", e); tracing::error!("Error handling connection: {}", e);
} }
} }

View file

@ -6,7 +6,7 @@ pub enum PathResolutionError {
NotFound, NotFound,
} }
pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16) -> Result<String, ()> { pub fn parse_gemini_url(request: &str, hostname: &str, expected_port: u16) -> Result<String, ()> {
if let Some(url) = request.strip_prefix("gemini://") { if let Some(url) = request.strip_prefix("gemini://") {
let host_port_end = url.find('/').unwrap_or(url.len()); let host_port_end = url.find('/').unwrap_or(url.len());
let host_port = &url[..host_port_end]; let host_port = &url[..host_port_end];
@ -21,7 +21,7 @@ pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16)
}; };
// Validate host // Validate host
if host != expected_host { if host != hostname {
return Err(()); // Hostname mismatch return Err(()); // Hostname mismatch
} }

View file

@ -40,7 +40,7 @@ pub async fn serve_file(
pub async fn handle_connection( pub async fn handle_connection(
mut stream: TlsStream<TcpStream>, mut stream: TlsStream<TcpStream>,
dir: &str, dir: &str,
expected_host: &str, hostname: &str,
expected_port: u16, expected_port: u16,
max_concurrent_requests: usize, max_concurrent_requests: usize,
_test_processing_delay: u64, _test_processing_delay: u64,
@ -97,7 +97,7 @@ pub async fn handle_connection(
} }
// Parse Gemini URL // Parse Gemini URL
let path = match parse_gemini_url(&request, expected_host, expected_port) { let path = match parse_gemini_url(&request, hostname, expected_port) {
Ok(p) => p, Ok(p) => p,
Err(_) => { Err(_) => {
logger.log_error(59, "Invalid URL format"); logger.log_error(59, "Invalid URL format");