Initial release v0.1.0

This commit is contained in:
Lihatoo 2026-08-21 18:34:18 +08:00
commit b997bd6078
62 changed files with 56224 additions and 0 deletions

397
src/application/setting.rs Normal file
View file

@ -0,0 +1,397 @@
use std::{
fs, io,
io::{IsTerminal, Write},
net::IpAddr,
};
use crate::domain::{RuntimeConfig, Settings};
use crate::platform::{AppLock, AppPaths};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Target {
Settings,
Runtime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ValueKind {
Bool,
Port,
PositiveInteger,
Ip,
LogLevel,
ProxyMode,
TunStack,
}
#[derive(Debug, Clone, Copy)]
struct SettingSpec {
key: &'static str,
target: Target,
kind: ValueKind,
}
const SPECS: &[SettingSpec] = &[
spec("bypass.enabled", Target::Settings, ValueKind::Bool),
spec("log.level", Target::Settings, ValueKind::LogLevel),
spec(
"log.max_size_mb",
Target::Settings,
ValueKind::PositiveInteger,
),
spec("proxy.mode", Target::Runtime, ValueKind::ProxyMode),
spec("proxy.listen", Target::Runtime, ValueKind::Ip),
spec("proxy.mixed_port", Target::Runtime, ValueKind::Port),
spec("proxy.http_port", Target::Runtime, ValueKind::Port),
spec("proxy.socks_port", Target::Runtime, ValueKind::Port),
spec("proxy.allow_lan", Target::Runtime, ValueKind::Bool),
spec("proxy.ipv6", Target::Runtime, ValueKind::Bool),
spec("api.enabled", Target::Runtime, ValueKind::Bool),
spec("api.listen", Target::Runtime, ValueKind::Ip),
spec("api.port", Target::Runtime, ValueKind::Port),
spec("dns.enabled", Target::Runtime, ValueKind::Bool),
spec("dns.listen", Target::Runtime, ValueKind::Ip),
spec("dns.port", Target::Runtime, ValueKind::Port),
spec("dns.ipv6", Target::Runtime, ValueKind::Bool),
spec("tun.stack", Target::Runtime, ValueKind::TunStack),
spec("tun.auto_route", Target::Runtime, ValueKind::Bool),
spec(
"tun.auto_detect_interface",
Target::Runtime,
ValueKind::Bool,
),
spec("tun.dns_hijack", Target::Runtime, ValueKind::Bool),
];
const fn spec(key: &'static str, target: Target, kind: ValueKind) -> SettingSpec {
SettingSpec { key, target, kind }
}
pub fn interactive(paths: &AppPaths) -> Result<(), io::Error> {
if !io::stdin().is_terminal() {
return list(paths);
}
let settings = Settings::load(&paths.settings_file())?;
let runtime = RuntimeConfig::load(&paths.runtime_file())?;
for (index, spec) in SPECS.iter().enumerate() {
println!(
"{:>2}) {:<28} {}",
index + 1,
spec.key,
get_value(spec.key, &settings, &runtime)?
);
}
let selected = prompt("选择项目0 取消): ")?;
let number = selected
.parse::<usize>()
.map_err(|_| invalid("请输入列表中的数字"))?;
if number == 0 {
return Ok(());
}
let spec = SPECS
.get(number.saturating_sub(1))
.ok_or_else(|| invalid("选择超出范围"))?;
let value = prompt(&format!("{} 新值: ", spec.key))?;
set(paths, spec.key, Some(&value))
}
pub fn list(paths: &AppPaths) -> Result<(), io::Error> {
let settings = Settings::load(&paths.settings_file())?;
let runtime = RuntimeConfig::load(&paths.runtime_file())?;
for spec in SPECS {
println!(
"{:<28} {:<8} {}",
spec.key,
kind_name(spec.kind),
get_value(spec.key, &settings, &runtime)?
);
}
Ok(())
}
pub fn get(paths: &AppPaths, key: &str) -> Result<(), io::Error> {
find_spec(key)?;
let settings = Settings::load(&paths.settings_file())?;
let runtime = RuntimeConfig::load(&paths.runtime_file())?;
println!("{}", get_value(key, &settings, &runtime)?);
Ok(())
}
pub fn set(paths: &AppPaths, key: &str, value: Option<&str>) -> Result<(), io::Error> {
let spec = find_spec(key)?;
let value = match value {
Some(value) => value.to_owned(),
None if io::stdin().is_terminal() => prompt(&format!("{key} 新值: "))?,
None => return Err(invalid("非交互调用必须提供 value")),
};
let _lock = AppLock::acquire(&paths.lock_file())?;
let mut settings = Settings::load(&paths.settings_file())?;
let mut runtime = RuntimeConfig::load(&paths.runtime_file())?;
set_value(key, &value, &mut settings, &mut runtime)?;
validate_runtime(&runtime)?;
match spec.target {
Target::Settings => settings.save(&paths.settings_file())?,
Target::Runtime => runtime.save(&paths.runtime_file())?,
}
invalidate_generated(paths)?;
println!("已保存 {key}={value};需要重新 build/start 后生效。");
Ok(())
}
pub fn reset(paths: &AppPaths, key: Option<&str>) -> Result<(), io::Error> {
let _lock = AppLock::acquire(&paths.lock_file())?;
let mut settings = Settings::load(&paths.settings_file())?;
let mut runtime = RuntimeConfig::load(&paths.runtime_file())?;
let defaults_settings = Settings::default();
let defaults_runtime = RuntimeConfig::default();
match key {
Some(key) => {
let spec = find_spec(key)?;
let value = get_value(key, &defaults_settings, &defaults_runtime)?;
set_value(key, &value, &mut settings, &mut runtime)?;
validate_runtime(&runtime)?;
match spec.target {
Target::Settings => settings.save(&paths.settings_file())?,
Target::Runtime => runtime.save(&paths.runtime_file())?,
}
println!("已恢复 {key}={value};需要重新 build/start 后生效。");
}
None => {
for spec in SPECS {
let value = get_value(spec.key, &defaults_settings, &defaults_runtime)?;
set_value(spec.key, &value, &mut settings, &mut runtime)?;
}
validate_runtime(&runtime)?;
settings.save(&paths.settings_file())?;
runtime.save(&paths.runtime_file())?;
println!("已恢复全部公开设置;需要重新 build/start 后生效。");
}
}
invalidate_generated(paths)?;
Ok(())
}
fn find_spec(key: &str) -> Result<&'static SettingSpec, io::Error> {
SPECS
.iter()
.find(|spec| spec.key == key)
.ok_or_else(|| invalid(format!("未知 setting key `{key}`")))
}
fn get_value(key: &str, settings: &Settings, runtime: &RuntimeConfig) -> Result<String, io::Error> {
let value = match key {
"bypass.enabled" => settings.bypass.enabled.to_string(),
"log.level" => settings.log.level.clone(),
"log.max_size_mb" => settings.log.max_size_mb.to_string(),
"proxy.mode" => runtime.proxy.mode.clone(),
"proxy.listen" => runtime.proxy.listen.clone(),
"proxy.mixed_port" => runtime.proxy.mixed_port.to_string(),
"proxy.http_port" => runtime.proxy.http_port.to_string(),
"proxy.socks_port" => runtime.proxy.socks_port.to_string(),
"proxy.allow_lan" => runtime.proxy.allow_lan.to_string(),
"proxy.ipv6" => runtime.proxy.ipv6.to_string(),
"api.enabled" => runtime.api.enabled.to_string(),
"api.listen" => runtime.api.listen.clone(),
"api.port" => runtime.api.port.to_string(),
"dns.enabled" => runtime.dns.enabled.to_string(),
"dns.listen" => runtime.dns.listen.clone(),
"dns.port" => runtime.dns.port.to_string(),
"dns.ipv6" => runtime.dns.ipv6.to_string(),
"tun.stack" => runtime.tun.stack.clone(),
"tun.auto_route" => runtime.tun.auto_route.to_string(),
"tun.auto_detect_interface" => runtime.tun.auto_detect_interface.to_string(),
"tun.dns_hijack" => runtime.tun.dns_hijack.to_string(),
_ => return Err(invalid(format!("未知 setting key `{key}`"))),
};
Ok(value)
}
fn set_value(
key: &str,
value: &str,
settings: &mut Settings,
runtime: &mut RuntimeConfig,
) -> Result<(), io::Error> {
let spec = find_spec(key)?;
validate_value(spec.kind, value)?;
match key {
"bypass.enabled" => settings.bypass.enabled = parse_bool(value)?,
"log.level" => settings.log.level = value.to_ascii_lowercase(),
"log.max_size_mb" => settings.log.max_size_mb = value.parse().map_err(parse_error)?,
"proxy.mode" => runtime.proxy.mode = value.to_ascii_lowercase(),
"proxy.listen" => runtime.proxy.listen = normalize_ip(value)?,
"proxy.mixed_port" => runtime.proxy.mixed_port = parse_port(value)?,
"proxy.http_port" => runtime.proxy.http_port = parse_port(value)?,
"proxy.socks_port" => runtime.proxy.socks_port = parse_port(value)?,
"proxy.allow_lan" => runtime.proxy.allow_lan = parse_bool(value)?,
"proxy.ipv6" => runtime.proxy.ipv6 = parse_bool(value)?,
"api.enabled" => runtime.api.enabled = parse_bool(value)?,
"api.listen" => runtime.api.listen = normalize_ip(value)?,
"api.port" => runtime.api.port = parse_port(value)?,
"dns.enabled" => runtime.dns.enabled = parse_bool(value)?,
"dns.listen" => runtime.dns.listen = normalize_ip(value)?,
"dns.port" => runtime.dns.port = parse_port(value)?,
"dns.ipv6" => runtime.dns.ipv6 = parse_bool(value)?,
"tun.stack" => runtime.tun.stack = value.to_ascii_lowercase(),
"tun.auto_route" => runtime.tun.auto_route = parse_bool(value)?,
"tun.auto_detect_interface" => runtime.tun.auto_detect_interface = parse_bool(value)?,
"tun.dns_hijack" => runtime.tun.dns_hijack = parse_bool(value)?,
_ => return Err(invalid(format!("未知 setting key `{key}`"))),
}
Ok(())
}
fn validate_value(kind: ValueKind, value: &str) -> Result<(), io::Error> {
match kind {
ValueKind::Bool => {
parse_bool(value)?;
}
ValueKind::Port => {
parse_port(value)?;
}
ValueKind::PositiveInteger => {
let number: u32 = value.parse().map_err(parse_error)?;
if number == 0 || number > 4096 {
return Err(invalid("数值必须在 1..=4096"));
}
}
ValueKind::Ip => {
normalize_ip(value)?;
}
ValueKind::LogLevel => {
if !matches!(
value.to_ascii_lowercase().as_str(),
"error" | "warn" | "info" | "debug" | "trace"
) {
return Err(invalid("日志级别必须是 error|warn|info|debug|trace"));
}
}
ValueKind::ProxyMode => {
if !matches!(
value.to_ascii_lowercase().as_str(),
"rule" | "global" | "direct"
) {
return Err(invalid("代理模式必须是 rule|global|direct"));
}
}
ValueKind::TunStack => {
if !matches!(
value.to_ascii_lowercase().as_str(),
"system" | "gvisor" | "mixed"
) {
return Err(invalid("TUN stack 必须是 system|gvisor|mixed"));
}
}
}
Ok(())
}
fn validate_runtime(runtime: &RuntimeConfig) -> Result<(), io::Error> {
let ports = [
("proxy.mixed_port", runtime.proxy.mixed_port),
("proxy.http_port", runtime.proxy.http_port),
("proxy.socks_port", runtime.proxy.socks_port),
("api.port", runtime.api.port),
("dns.port", runtime.dns.port),
];
for (index, (left_name, left)) in ports.iter().enumerate() {
for (right_name, right) in ports.iter().skip(index + 1) {
if left == right {
return Err(invalid(format!(
"端口冲突:{left_name} 和 {right_name} 都是 {left}"
)));
}
}
}
Ok(())
}
fn parse_bool(value: &str) -> Result<bool, io::Error> {
match value.to_ascii_lowercase().as_str() {
"true" | "on" => Ok(true),
"false" | "off" => Ok(false),
_ => Err(invalid("布尔值必须是 on|off|true|false")),
}
}
fn parse_port(value: &str) -> Result<u16, io::Error> {
let port: u16 = value.parse().map_err(parse_error)?;
if port == 0 {
return Err(invalid("端口必须在 1..=65535"));
}
Ok(port)
}
fn normalize_ip(value: &str) -> Result<String, io::Error> {
value
.parse::<IpAddr>()
.map(|ip| ip.to_string())
.map_err(|_| invalid(format!("无效 IP 地址 `{value}`")))
}
fn parse_error(error: std::num::ParseIntError) -> io::Error {
invalid(error.to_string())
}
fn invalidate_generated(paths: &AppPaths) -> Result<(), io::Error> {
let generated = paths.generated_dir();
if generated.is_dir() {
fs::remove_dir_all(&generated)?;
}
fs::create_dir_all(generated)
}
fn prompt(message: &str) -> Result<String, io::Error> {
print!("{message}");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_owned())
}
fn kind_name(kind: ValueKind) -> &'static str {
match kind {
ValueKind::Bool => "bool",
ValueKind::Port => "port",
ValueKind::PositiveInteger => "integer",
ValueKind::Ip => "ip",
ValueKind::LogLevel | ValueKind::ProxyMode | ValueKind::TunStack => "enum",
}
}
fn invalid(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message.into())
}
#[cfg(test)]
mod tests {
use super::{SPECS, set_value, validate_runtime};
use crate::domain::{RuntimeConfig, Settings};
use std::collections::HashSet;
#[test]
fn registry_keys_are_unique() {
let mut keys = HashSet::new();
assert!(SPECS.iter().all(|spec| keys.insert(spec.key)));
}
#[test]
fn accepts_typed_values_and_rejects_unknown_keys() {
let mut settings = Settings::default();
let mut runtime = RuntimeConfig::default();
set_value("proxy.mode", "global", &mut settings, &mut runtime).unwrap();
set_value("proxy.allow_lan", "on", &mut settings, &mut runtime).unwrap();
assert_eq!(runtime.proxy.mode, "global");
assert!(runtime.proxy.allow_lan);
assert!(set_value("active.tun", "on", &mut settings, &mut runtime).is_err());
}
#[test]
fn rejects_port_conflicts() {
let mut runtime = RuntimeConfig::default();
runtime.api.port = runtime.proxy.mixed_port;
assert!(validate_runtime(&runtime).is_err());
}
}