Gemini 协议作为一种轻量级、注重隐私的网络协议,提供了一种简洁的替代方案,用于传输文本和简单媒体内容,而无需依赖 cookies 或 JavaScript 等复杂机制。它运行在 TCP 端口 1965 上,并强制使用 TLS 加密,确保所有通信安全且无跟踪。在 Rust 这种系统级语言中实现一个最小化 Gemini 服务器,可以充分利用其内存安全和高性能特性,处理证书管理、请求解析和响应序列化等核心组件,实现高效的隐私导向 web 服务。
Gemini 协议的核心机制
Gemini 的设计哲学强调最小主义:客户端发送一个简单的绝对 URI 请求(以 CRLF 结束),服务器响应一个两位的状态码、MIME 类型元数据(可选)和响应体,然后关闭连接。协议规格(v0.24.1)定义了 10-69 的状态码范围,例如 20 表示成功(Success),附加 MIME 类型如 text/gemini;40 表示临时失败(Temporary Failure)。请求 URI 长度上限为 1024 字节,服务器必须拒绝包含 userinfo 或 fragment 的无效请求。
证据显示,这种简单性源于对 Gopher 协议的改进,同时避免 HTTP 的复杂性。Gemini 强制 TLS 1.2+,推荐 Trust On First Use (TOFU) 证书验证:客户端首次连接时接受服务器证书的指纹,后续验证匹配,以防范中间人攻击。无状态设计确保隐私:无会话跟踪、无 JavaScript 执行,响应体直接为原始内容,支持 text/gemini(Gemtext)格式的超文本。
在 Rust 中实现时,我们可以使用 tokio 处理异步 I/O,rustls 管理 TLS 证书,避免 OpenSSL 的依赖以保持轻量。实际项目如 Agate(一个纯 Rust 的静态文件服务器)和 Aerozine(支持动态内容和多域配置)证明了这种方法的有效性:Agate 通过加载文件到内存实现快速服务,Aerozine 使用 JSON 配置处理证书和路由。
TLS 证书管理:安全基础
证书管理是 Gemini 服务器的起点,因为协议要求所有连接加密。Rust 中的 rustls 库提供原生 TLS 支持,无需外部 C 库,减少攻击面。生成自签名证书是开发阶段的常见实践,使用 openssl 命令:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=example.com"
服务器加载这些文件:私钥 (key.pem) 用于解密握手,公钥证书 (cert.pem) 用于验证身份。rustls::ServerConfig 配置示例:
let certs = load_certs("cert.pem")?;
let mut keys = load_private_key("key.pem")?;
let config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, keys)?;
可落地参数:
- cert_path: 证书文件路径,默认 "cert.pem",支持 PEM 格式,支持链式证书以处理多域。
- key_path: 私钥路径,默认 "key.pem",必须匹配证书,避免弱加密算法(推荐 RSA 2048+ 或 ECDSA)。
- tofu_enabled: 布尔值,启用 TOFU 时,服务器可选记录客户端证书指纹,用于访问控制(状态码 60-62)。
- cert_expiry_check: 间隔检查证书过期(e.g., 每日),若过期则返回 51 (Not Found) 或日志警告。
- sni_support: 启用 Server Name Indication,支持虚拟主机(多域),rustls 通过扩展处理。
风险:自签名证书易受 TOFU 绕过攻击,生产环境推荐 Let's Encrypt ACME 自动化续期。监控点:日志客户端证书指纹不匹配(潜在 MITM),阈值:5 次/分钟触发警报。
请求解析:高效 URI 处理
请求是单行 UTF-8 字符串:gemini://host:port/path?query CRLF。Rust 使用 tokio::net::TcpStream 读取,解析为 URI 结构。使用 url crate 验证:
let request = read_line(stream).await?;
let parsed = Url::parse(&request.trim_end_matches('\n'))?;
if parsed.scheme() != "gemini" || parsed.port().unwrap_or(1965) != 1965 {
return send_error(59, "Bad Request");
}
let path = parsed.path();
let query = parsed.query().unwrap_or("");
解析后,检查路径:空路径或 "/" 等价于根索引。拒绝超过 1024 字节或含 fragment 的请求,返回 59 (Bad Request)。对于动态内容,query 作为参数传递给 CGI-like 处理程序。
证据:协议规格要求服务器处理相对重定向(30/31),但最小服务器可忽略,仅服务静态路径。Aerozine 示例中,使用 HashMap 映射路径到文件或生成器,确保无路径遍历(normalize path, reject "..")。
可落地参数/清单:
- max_request_size: 1024 字节,默认,超出返回 59。
- path_root: 根目录 e.g., "./public",使用 std::fs::canonicalize 防止遍历。
- query_encoding: URI 解码 query,使用 percent_encoding crate,替换 %20 为空格。
- request_log: 启用日志 {timestamp, ip, path, query},格式 JSON,便于监控。
- rate_limit: 每 IP 10 请求/分钟,超出返回 44 (Slow Down),使用 tokio::time::timeout。
回滚策略:若解析失败,fallback 到默认 50 (Permanent Failure),日志原始请求以调试。
响应序列化:简洁输出
响应格式:{status} {meta} CRLF {body},然后 TLS close_notify 关闭。状态码决定 meta:成功 (20) 为 MIME e.g., "text/gemini; charset=utf-8";重定向 (30/31) 为 URI;输入 (10/11) 为提示文本。Gemtext 响应避免 BOM (U+FEFF),使用 CRLF 或 LF 行结束。
Rust 序列化使用 String 构建头,tokio::io::AsyncWriteExt 写入 body。示例:
let mut response = format!("20 text/gemini\r\n");
response.push_str(&body);
stream.write_all(response.as_bytes()).await?;
对于文件服务,使用 tokio::fs::File::open,异步读取。动态响应:执行外部程序,捕获 stdout 作为 body。
证据:Gemini 规范强调无压缩/分块,响应体原始传输。Pollux (Rust 服务器) 示例中,使用 futures::stream::once 流式 body,支持大文件而不缓冲全部内存。
可落地参数/清单:
- default_mime: "text/gemini" for .gmi, "text/plain" for others, 使用 mime_guess crate 自动检测。
- status_custom: 动态内容允许自定义状态 (e.g., 42 for CGI error),范围 40-59。
- body_max_size: 响应体上限 10MB,超出截断或 51,返回 40。
- charset_default: "utf-8",文本类型必须指定,避免 US-ASCII 假设。
- close_notify: 始终使用 rustls::StreamOwned::get_ref().close() 优雅关闭。
监控点:响应时间 < 100ms (静态),> 500ms 日志警告;错误率 > 5% 触发重启。
工程化实现与最佳实践
构建最小服务器:依赖 Cargo.toml: tokio = { version = "1", features = ["full"] }, rustls = "0.21", url = "2"。主循环:tokio::net::TcpListener 绑定 0.0.0.0:1965,接受连接,rustls::ServerConnection 处理 TLS,解析请求,服务路径(fs::read_to_string for static),序列化响应。
完整清单:
- 生成/加载证书:rustls 配置,TOFU 数据库 (sled DB)。
- 异步服务器:tokio spawn 每个连接。
- 路径服务:match path { "/" => index.gmi, "/file" => serve_file }。
- 错误处理:统一 send_response( status, meta, body )。
- 配置:TOML 文件,clap 解析 CLI args (e.g., --cert cert.pem --port 1965)。
测试:使用 curl --tlsv1.2 -k gemini://localhost:1965/,或自定义客户端。性能:单核处理 1000+ req/s (静态)。
这种实现确保服务器轻量(< 500 LOC),隐私优先,无不必要依赖。扩展时,添加动态 CGI via Command::new("./script.sh").arg(query)。
资料来源:Gemini 协议规范 (geminiprotocol.net/docs/protocol-specification.gmi);Rust 库 tokio, rustls;开源项目 Agate (github.com/jeffreymk/agate), Aerozine (github.com/slogemann1/aerozine)。