mirror of https://github.com/ospab/ostp.git
262 lines
7.9 KiB
Rust
262 lines
7.9 KiB
Rust
use anyhow::{Context, Result};
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
/// Client runtime configuration.
|
||
/// Constructed by the main binary from the unified `config.json`,
|
||
/// then passed into `runner::run_client`. All I/O happens in the
|
||
/// binary layer — this crate only owns the plain data structures.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ClientConfig {
|
||
pub mode: String,
|
||
#[serde(default)]
|
||
pub debug: bool,
|
||
pub ostp: OstpConfig,
|
||
pub local_proxy: LocalProxyConfig,
|
||
#[serde(default)]
|
||
pub transport: TransportConfig,
|
||
#[serde(default)]
|
||
pub exclusions: ExclusionConfig,
|
||
#[serde(default)]
|
||
pub multiplex: MultiplexConfig,
|
||
pub dns_server: Option<String>,
|
||
#[serde(default = "default_tun_stack")]
|
||
pub tun_stack: String,
|
||
#[serde(default)]
|
||
pub kill_switch: bool,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub gui: Option<serde_json::Value>,
|
||
}
|
||
|
||
fn default_tun_stack() -> String { "system".to_string() }
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||
pub struct ExclusionConfig {
|
||
#[serde(default)]
|
||
pub domains: Vec<String>,
|
||
#[serde(default)]
|
||
pub ips: Vec<String>,
|
||
#[serde(default)]
|
||
pub processes: Vec<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct MultiplexConfig {
|
||
pub enabled: bool,
|
||
pub sessions: usize,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct OstpConfig {
|
||
pub server_addr: String,
|
||
pub local_bind_addr: String,
|
||
#[serde(alias = "auth_token")]
|
||
pub access_key: String,
|
||
pub handshake_timeout_ms: u64,
|
||
pub io_timeout_ms: u64,
|
||
#[serde(default = "default_mtu")]
|
||
pub mtu: usize,
|
||
#[serde(default = "default_keepalive")]
|
||
pub keepalive_interval_sec: u64,
|
||
}
|
||
|
||
fn default_keepalive() -> u64 { 5 }
|
||
|
||
fn default_mtu() -> usize { 1140 }
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct LocalProxyConfig {
|
||
pub bind_addr: String,
|
||
pub connect_timeout_ms: u64,
|
||
}
|
||
|
||
/// Transport layer configuration.
|
||
/// `mode` = "udp" (default) or "uot" (UDP over TCP с xHTTP-транспортом).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct TransportConfig {
|
||
/// "udp" or "uot"
|
||
#[serde(default = "default_transport_mode")]
|
||
pub mode: String,
|
||
/// TLS SNI and HTTP Host for xHTTP routing
|
||
#[serde(default)]
|
||
pub stealth_sni: String,
|
||
}
|
||
|
||
fn default_transport_mode() -> String { "udp".to_string() }
|
||
|
||
impl Default for TransportConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
mode: default_transport_mode(),
|
||
stealth_sni: String::new(),
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
impl Default for OstpConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
server_addr: "127.0.0.1:50000".to_string(),
|
||
local_bind_addr: "0.0.0.0:0".to_string(),
|
||
access_key: String::new(),
|
||
handshake_timeout_ms: 5000,
|
||
io_timeout_ms: 2500,
|
||
mtu: default_mtu(),
|
||
keepalive_interval_sec: default_keepalive(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for LocalProxyConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
bind_addr: "127.0.0.1:1088".to_string(),
|
||
connect_timeout_ms: 15000,
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
impl Default for ClientConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
mode: "proxy".to_string(),
|
||
debug: false,
|
||
ostp: OstpConfig::default(),
|
||
local_proxy: LocalProxyConfig::default(),
|
||
transport: TransportConfig::default(),
|
||
exclusions: ExclusionConfig::default(),
|
||
multiplex: MultiplexConfig::default(),
|
||
dns_server: None,
|
||
tun_stack: "system".to_string(),
|
||
kill_switch: false,
|
||
gui: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for MultiplexConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: false,
|
||
sessions: 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Unified shape of `config.json` as seen by the client.
|
||
/// Used only for hot-reloading (`BridgeCommand::ReloadConfig`).
|
||
#[derive(Debug, Deserialize)]
|
||
struct RawUnifiedConfig {
|
||
#[allow(dead_code)]
|
||
mode: String,
|
||
debug: Option<bool>,
|
||
server: Option<String>,
|
||
access_key: Option<String>,
|
||
mtu: Option<usize>,
|
||
socks5_bind: Option<String>,
|
||
tun: Option<RawTunSection>,
|
||
exclude: Option<RawExcludeSection>,
|
||
mux: Option<RawMuxSection>,
|
||
transport: Option<RawTransportSection>,
|
||
gui: Option<serde_json::Value>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
struct RawTransportSection {
|
||
mode: Option<String>,
|
||
stealth_sni: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
struct RawTunSection {
|
||
enable: Option<bool>,
|
||
dns: Option<String>,
|
||
stack: Option<String>,
|
||
kill_switch: Option<bool>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
struct RawExcludeSection {
|
||
domains: Option<Vec<String>>,
|
||
ips: Option<Vec<String>>,
|
||
processes: Option<Vec<String>>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
struct RawMuxSection {
|
||
enabled: Option<bool>,
|
||
sessions: Option<usize>,
|
||
}
|
||
|
||
|
||
|
||
impl ClientConfig {
|
||
/// Hot-reload from `config.json` placed next to the running binary.
|
||
/// Returns a new `ClientConfig` built from the unified JSON format.
|
||
pub fn reload_from_json_near_binary() -> Result<Self> {
|
||
let exe = std::env::current_exe().context("cannot resolve binary path")?;
|
||
let dir = exe.parent().context("cannot resolve binary directory")?;
|
||
let path = dir.join("config.json");
|
||
|
||
let raw = std::fs::read_to_string(&path)
|
||
.with_context(|| format!("failed to read {}", path.display()))?;
|
||
let mut stripped = json_comments::StripComments::new(raw.as_bytes());
|
||
let raw: RawUnifiedConfig = serde_json::from_reader(&mut stripped)
|
||
.with_context(|| format!("failed to parse {}", path.display()))?;
|
||
|
||
let is_tun = raw.tun.as_ref().and_then(|t| t.enable).unwrap_or(false);
|
||
let server = raw.server.unwrap_or_else(|| "127.0.0.1:50000".to_string());
|
||
let key = raw.access_key.unwrap_or_default();
|
||
let mtu = raw.mtu.unwrap_or(default_mtu());
|
||
let socks5 = raw.socks5_bind.unwrap_or_else(|| "127.0.0.1:1088".to_string());
|
||
let exclusions = raw.exclude.unwrap_or(RawExcludeSection {
|
||
domains: None,
|
||
ips: None,
|
||
processes: None,
|
||
});
|
||
let mux = raw.mux.unwrap_or(RawMuxSection {
|
||
enabled: None,
|
||
sessions: None,
|
||
});
|
||
|
||
Ok(ClientConfig {
|
||
mode: if is_tun { "tun".to_string() } else { "proxy".to_string() },
|
||
debug: raw.debug.unwrap_or(false),
|
||
ostp: OstpConfig {
|
||
server_addr: server,
|
||
local_bind_addr: "0.0.0.0:0".to_string(),
|
||
access_key: key,
|
||
handshake_timeout_ms: 5000,
|
||
io_timeout_ms: 2500,
|
||
mtu,
|
||
keepalive_interval_sec: default_keepalive(),
|
||
},
|
||
local_proxy: LocalProxyConfig {
|
||
bind_addr: socks5,
|
||
connect_timeout_ms: 15000,
|
||
},
|
||
transport: TransportConfig {
|
||
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode),
|
||
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(),
|
||
},
|
||
exclusions: ExclusionConfig {
|
||
domains: exclusions.domains.unwrap_or_default(),
|
||
ips: exclusions.ips.unwrap_or_default(),
|
||
processes: exclusions.processes.unwrap_or_default(),
|
||
},
|
||
multiplex: MultiplexConfig {
|
||
enabled: mux.enabled.unwrap_or(false),
|
||
sessions: mux.sessions.unwrap_or(1),
|
||
},
|
||
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
|
||
tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()),
|
||
kill_switch: raw.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
|
||
gui: raw.gui,
|
||
})
|
||
}
|
||
}
|