refactor(proxy): 简化 HMAC 认证、移除明文 HTTP 代理并优化 setup 流程

- HMAC 签名移除 node_id,仅使用 timestamp,消除重注册时的认证竞态问题
- 删除 plain HTTP forward proxy(plain.rs),仅保留 CONNECT 隧道和 delegate,非支持方法返回 405
- 提取共享 BoxBody 类型和 empty_box_body() 到 proxy/mod.rs
- CLI 解析重构为 clap 原生 subcommand,启用 subcommand_negates_reqs
- setup 向导返回 SetupOutcome 枚举,支持保存后自动启动 proxy
- setup TUI 增加未保存变更的退出确认(pending_quit)
- validate_target 改为 async,使用 tokio::net::lookup_host 避免阻塞 DNS
- 显式初始化 rustls ring CryptoProvider
- ConfigFile 新增 inject_env_override() 用于 setup 后重载配置
- Python 端 HMAC 签名同步移除 node_id,缓存时间桶从 120s 调整为 240s
This commit is contained in:
fawney19
2026-02-08 16:10:13 +08:00
parent f07cae540e
commit 183e7c60e1
15 changed files with 261 additions and 357 deletions

View File

@@ -37,7 +37,11 @@ impl std::fmt::Display for AuthError {
/// Validate Proxy-Authorization header.
///
/// Expected format: `Basic base64(hmac:{timestamp}.{signature})`
/// where signature = hex(HMAC-SHA256(hmac_key, "{timestamp}\n{node_id}"))
/// where signature = hex(HMAC-SHA256(hmac_key, "{timestamp}"))
///
/// The signature no longer includes `node_id`, eliminating race conditions
/// during re-registration where the Aether server's cached `node_id` could
/// differ from the proxy's freshly assigned `node_id`.
///
/// `timestamp_tolerance` is accepted separately so the caller can supply
/// the value from [`DynamicConfig`](crate::runtime::DynamicConfig) (which
@@ -45,7 +49,6 @@ impl std::fmt::Display for AuthError {
pub fn validate_proxy_auth(
proxy_auth_header: Option<&str>,
config: &Config,
node_id: &str,
timestamp_tolerance: u64,
) -> Result<(), AuthError> {
let header = proxy_auth_header.ok_or(AuthError::MissingHeader)?;
@@ -88,11 +91,10 @@ pub fn validate_proxy_auth(
return Err(AuthError::TimestampExpired);
}
// Recompute signature
let payload = format!("{}\n{}", timestamp_str, node_id);
// Recompute signature: HMAC-SHA256(key, timestamp)
let mut mac =
HmacSha256::new_from_slice(config.hmac_key.as_bytes()).expect("HMAC accepts any key size");
mac.update(payload.as_bytes());
mac.update(timestamp_str.as_bytes());
let expected = mac.finalize().into_bytes();
let expected_hex = hex::encode(expected);
@@ -131,14 +133,13 @@ mod tests {
}
}
fn make_valid_auth(config: &Config, node_id: &str) -> String {
fn make_valid_auth(config: &Config) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let payload = format!("{}\n{}", now, node_id);
let mut mac = HmacSha256::new_from_slice(config.hmac_key.as_bytes()).unwrap();
mac.update(payload.as_bytes());
mac.update(now.to_string().as_bytes());
let sig = hex::encode(mac.finalize().into_bytes());
let cred = format!("hmac:{}.{}", now, sig);
let encoded = base64::engine::general_purpose::STANDARD.encode(cred);
@@ -148,28 +149,15 @@ mod tests {
#[test]
fn test_valid_auth() {
let config = make_config();
let header = make_valid_auth(&config, "node-1");
assert!(
validate_proxy_auth(Some(&header), &config, "node-1", config.timestamp_tolerance)
.is_ok()
);
}
#[test]
fn test_wrong_node_id() {
let config = make_config();
let header = make_valid_auth(&config, "node-1");
assert!(matches!(
validate_proxy_auth(Some(&header), &config, "node-2", config.timestamp_tolerance),
Err(AuthError::SignatureMismatch)
));
let header = make_valid_auth(&config);
assert!(validate_proxy_auth(Some(&header), &config, config.timestamp_tolerance).is_ok());
}
#[test]
fn test_missing_header() {
let config = make_config();
assert!(matches!(
validate_proxy_auth(None, &config, "node-1", config.timestamp_tolerance),
validate_proxy_auth(None, &config, config.timestamp_tolerance),
Err(AuthError::MissingHeader)
));
}
@@ -181,7 +169,7 @@ mod tests {
let header = format!("Basic {}", encoded);
let config = make_config();
assert!(matches!(
validate_proxy_auth(Some(&header), &config, "node-1", config.timestamp_tolerance),
validate_proxy_auth(Some(&header), &config, config.timestamp_tolerance),
Err(AuthError::InvalidUsername)
));
}