mirror of https://github.com/ospab/ostp.git
docs: Update config format to modular architecture v0.3.1
This commit is contained in:
parent
580faf659a
commit
8ed66f9553
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Миграция конфигурации OSTP v0.3.1
|
||||||
|
|
||||||
|
В версии OSTP 0.3.1 мы полностью переработали архитектуру конфигурации `config.json` для клиента. Старая монолитная структура (где все настройки были в корневом объекте) заменена на модульную систему на базе массивов `inbounds` (входящие соединения) и `outbounds` (исходящие соединения), аналогично Xray/V2Ray/Sing-box.
|
||||||
|
|
||||||
|
Это позволяет OSTP масштабироваться, поддерживать несколько прокси-серверов, несколько точек входа (SOCKS5, TUN) и сложную маршрутизацию (`routing`).
|
||||||
|
|
||||||
|
## Автоматическая миграция
|
||||||
|
|
||||||
|
В ядро `ostp` встроен автоматический мигратор. При запуске любой программы (cli, gui, flutter) ядро проверит ваш `config.json`.
|
||||||
|
|
||||||
|
Если в конфигурации отсутствует поле `"version": "0.3.1"`, OSTP **автоматически** конвертирует ваш старый конфиг в новый модульный формат и сохранит его на диск без потери данных.
|
||||||
|
|
||||||
|
### Что происходит при миграции:
|
||||||
|
|
||||||
|
1. **TUN и SOCKS5** -> преобразуются в массив `inbounds`.
|
||||||
|
- Настройка `socks5_bind` становится входящим `local_proxy` (SOCKS).
|
||||||
|
- Настройка `tun` становится входящим `tun`.
|
||||||
|
2. **Сервер OSTP** -> переносится в массив `outbounds`.
|
||||||
|
- Параметры `server`, `access_key`, `transport`, `mux` объединяются в `outbound` с типом `"ostp"`.
|
||||||
|
3. **Split Tunneling (Exclude)** -> преобразуется в `routing` правила.
|
||||||
|
- Старые `domains` и `ips` конвертируются в правила, направляющие трафик в `"direct"` outbound.
|
||||||
|
- Все остальные запросы по умолчанию направляются в `"proxy"` outbound.
|
||||||
|
4. **Поля `version`**
|
||||||
|
- Добавляется поле `"version": "0.3.1"`, чтобы предотвратить повторную миграцию в будущем. Поле `_comment` было удалено.
|
||||||
|
|
||||||
|
## Пример изменения
|
||||||
|
|
||||||
|
### До 0.3.1 (Старый формат)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "client",
|
||||||
|
"log_level": "info",
|
||||||
|
"server": "1.2.3.4:50000",
|
||||||
|
"access_key": "secret",
|
||||||
|
"socks5_bind": "127.0.0.1:1088",
|
||||||
|
"tun": {
|
||||||
|
"enable": true
|
||||||
|
},
|
||||||
|
"exclude": {
|
||||||
|
"domains": ["localhost"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### После 0.3.1 (Новый формат)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "client",
|
||||||
|
"version": "0.3.1",
|
||||||
|
"log": {
|
||||||
|
"level": "info"
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"auto_route": true,
|
||||||
|
"mtu": 1140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "local_proxy",
|
||||||
|
"tag": "socks-in",
|
||||||
|
"protocol": "socks",
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"port": 1088
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "ostp",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": "1.2.3.4",
|
||||||
|
"port": 50000,
|
||||||
|
"access_key": "secret",
|
||||||
|
"transport": {
|
||||||
|
"type": "udp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routing": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"domain_suffix": ["localhost"],
|
||||||
|
"outbound": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_outbound": "proxy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Информация для разработчиков GUI (ostp-gui, ostp-flutter)
|
||||||
|
|
||||||
|
Если вы разрабатываете интеграции или сторонние клиенты, **вам больше не нужно парсить старые поля**. Вы должны использовать массивы `inbounds` и `outbounds`. Если GUI передает `serde_json::Value` в ядро, ядро само проведет миграцию перед запуском. Однако для сохранения изменений из UI вы должны изменять именно новую структуру массивов.
|
||||||
31
README.md
31
README.md
|
|
@ -118,14 +118,35 @@ graph TD
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"server": "YOUR_SERVER_IP:50000",
|
"version": "0.3.1",
|
||||||
"access_key": "YOUR_SECRET_KEY",
|
"log": { "level": "info" },
|
||||||
"socks5_bind": "127.0.0.1:1088",
|
"inbounds": [
|
||||||
"transport": { "mode": "udp", "stealth_sni": "vk.com" },
|
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 },
|
||||||
"tun": { "enable": false, "dns": "1.1.1.1" }
|
{ "type": "tun", "tag": "tun-in", "auto_route": false, "mtu": 1140 }
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "ostp",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": "YOUR_SERVER_IP",
|
||||||
|
"port": 50000,
|
||||||
|
"access_key": "YOUR_SECRET_KEY",
|
||||||
|
"transport": { "type": "udp" }
|
||||||
|
},
|
||||||
|
{ "type": "direct", "tag": "direct" },
|
||||||
|
{ "type": "block", "tag": "block" }
|
||||||
|
],
|
||||||
|
"routing": {
|
||||||
|
"rules": [
|
||||||
|
{ "domain_suffix": ["localhost"], "outbound": "direct" }
|
||||||
|
],
|
||||||
|
"default_outbound": "proxy"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note:** Upgrading from v0.2.x? Read the [v0.3.1 Migration Guide](MIGRATION_V0_3_1.md).
|
||||||
|
|
||||||
### 3. Run
|
### 3. Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
57
README.ru.md
57
README.ru.md
|
|
@ -110,40 +110,37 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"server": "IP_СЕРВЕРА:50000",
|
"version": "0.3.1",
|
||||||
"access_key": "ВАШ_КЛЮЧ",
|
"log": { "level": "info" },
|
||||||
"socks5_bind": "127.0.0.1:1088",
|
"inbounds": [
|
||||||
"debug": false,
|
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 },
|
||||||
// Настройки транспорта (udp или uot)
|
{ "type": "tun", "tag": "tun-in", "auto_route": false, "mtu": 1140 }
|
||||||
"transport": {
|
],
|
||||||
"mode": "udp",
|
"outbounds": [
|
||||||
"stealth_sni": "vk.com"
|
{
|
||||||
},
|
"type": "ostp",
|
||||||
// TUN-режим (полносистемный VPN)
|
"tag": "proxy",
|
||||||
"tun": {
|
"server": "IP_СЕРВЕРА",
|
||||||
"enable": false,
|
"port": 50000,
|
||||||
"dns": "1.1.1.1"
|
"access_key": "ВАШ_КЛЮЧ",
|
||||||
},
|
"transport": { "type": "udp" },
|
||||||
// Мультиплексирование: несколько UDP-сессий
|
"multiplex": { "enabled": false, "sessions": 1 }
|
||||||
"mux": {
|
},
|
||||||
"enabled": false,
|
{ "type": "direct", "tag": "direct" },
|
||||||
"sessions": 2
|
{ "type": "block", "tag": "block" }
|
||||||
},
|
],
|
||||||
// TURN-реле для заблокированных сетей
|
"routing": {
|
||||||
"turn": {
|
"rules": [
|
||||||
"enabled": false,
|
{ "domain_suffix": ["example.local"], "outbound": "direct" },
|
||||||
"server_addr": "turn.example.com:3478",
|
{ "ip_cidr": ["192.168.0.0/16"], "outbound": "direct" }
|
||||||
"username": "user",
|
],
|
||||||
"access_key": "pass"
|
"default_outbound": "proxy"
|
||||||
},
|
|
||||||
// Исключения (идут напрямую, минуя туннель)
|
|
||||||
"exclude": {
|
|
||||||
"domains": ["example.local"],
|
|
||||||
"ips": ["192.168.0.0/16"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Примечание:** Обновляетесь с v0.2.x? Прочтите [Гайд по миграции на v0.3.1](MIGRATION_V0_3_1.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,17 @@ pub struct BridgeMetrics {
|
||||||
pub rtt_ms: AtomicU32,
|
pub rtt_ms: AtomicU32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for BridgeMetrics {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
|
bytes_recv: portable_atomic::AtomicU64::new(0),
|
||||||
|
connection_state: portable_atomic::AtomicU8::new(0),
|
||||||
|
rtt_ms: portable_atomic::AtomicU32::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_socket_protector<F>(f: F)
|
pub fn set_socket_protector<F>(f: F)
|
||||||
where
|
where
|
||||||
F: Fn(i32) -> bool + Send + Sync + 'static,
|
F: Fn(i32) -> bool + Send + Sync + 'static,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub version: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub log: LogConfig,
|
pub log: LogConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -153,9 +155,150 @@ impl ClientConfig {
|
||||||
let raw = std::fs::read_to_string(&path)
|
let raw = std::fs::read_to_string(&path)
|
||||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||||
let mut stripped = json_comments::StripComments::new(raw.as_bytes());
|
let mut stripped = json_comments::StripComments::new(raw.as_bytes());
|
||||||
let config: ClientConfig = serde_json::from_reader(&mut stripped)
|
let raw_json: serde_json::Value = serde_json::from_reader(&mut stripped)
|
||||||
.with_context(|| format!("failed to parse {}", path.display()))?;
|
.with_context(|| format!("failed to parse JSON from {}", path.display()))?;
|
||||||
|
|
||||||
|
let (migrated_json, was_migrated) = Self::migrate_json(raw_json);
|
||||||
|
|
||||||
|
if was_migrated {
|
||||||
|
tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display());
|
||||||
|
let serialized = serde_json::to_string_pretty(&migrated_json)?;
|
||||||
|
std::fs::write(&path, serialized)
|
||||||
|
.with_context(|| format!("failed to save migrated config to {}", path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ClientConfig = serde_json::from_value(migrated_json)
|
||||||
|
.with_context(|| format!("failed to deserialize migrated config from {}", path.display()))?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrates old monolithic JSON to the new modular format.
|
||||||
|
/// Returns the migrated JSON value and a boolean indicating if a migration occurred.
|
||||||
|
pub fn migrate_json(mut json: serde_json::Value) -> (serde_json::Value, bool) {
|
||||||
|
let is_migrated = json.get("version").and_then(|v| v.as_str()) == Some("0.3.1");
|
||||||
|
if is_migrated {
|
||||||
|
return (json, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs migration
|
||||||
|
let mut new_json = serde_json::json!({
|
||||||
|
"version": "0.3.1",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Log level
|
||||||
|
let log_level = if let Some(ll) = json.get("log_level") {
|
||||||
|
ll.clone()
|
||||||
|
} else if let Some(d) = json.get("debug") {
|
||||||
|
if d.as_bool().unwrap_or(false) { serde_json::json!("debug") } else { serde_json::json!("info") }
|
||||||
|
} else {
|
||||||
|
serde_json::json!("info")
|
||||||
|
};
|
||||||
|
new_json["log"] = serde_json::json!({ "level": log_level });
|
||||||
|
|
||||||
|
// 2. Inbounds
|
||||||
|
let mut inbounds = Vec::new();
|
||||||
|
|
||||||
|
if let Some(tun) = json.get("tun") {
|
||||||
|
if tun.get("enable").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||||
|
inbounds.push(serde_json::json!({
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"auto_route": true,
|
||||||
|
"mtu": 1140
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let socks_bind = json.get("socks5_bind").and_then(|v| v.as_str()).unwrap_or("127.0.0.1:1088");
|
||||||
|
let parts: Vec<&str> = socks_bind.split(':').collect();
|
||||||
|
let listen = parts.get(0).unwrap_or(&"127.0.0.1");
|
||||||
|
let port = parts.get(1).unwrap_or(&"1088").parse::<u16>().unwrap_or(1088);
|
||||||
|
|
||||||
|
inbounds.push(serde_json::json!({
|
||||||
|
"type": "local_proxy",
|
||||||
|
"tag": "socks-in",
|
||||||
|
"protocol": "socks",
|
||||||
|
"listen": listen,
|
||||||
|
"port": port
|
||||||
|
}));
|
||||||
|
|
||||||
|
new_json["inbounds"] = serde_json::Value::Array(inbounds);
|
||||||
|
|
||||||
|
// 3. Outbounds
|
||||||
|
let mut outbounds = Vec::new();
|
||||||
|
let server_full = json.get("server").and_then(|v| v.as_str()).unwrap_or("127.0.0.1:50000");
|
||||||
|
let server_parts: Vec<&str> = server_full.split(':').collect();
|
||||||
|
let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1");
|
||||||
|
let server_port = server_parts.get(1).unwrap_or(&"50000").parse::<u16>().unwrap_or(50000);
|
||||||
|
let access_key = json.get("access_key").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
let transport_type = json.get("transport").and_then(|t| t.get("mode").or(t.get("type"))).and_then(|v| v.as_str()).unwrap_or("udp");
|
||||||
|
let mux_enabled = json.get("mux").and_then(|m| m.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
let mux_sessions = json.get("mux").and_then(|m| m.get("sessions")).and_then(|v| v.as_u64()).unwrap_or(1);
|
||||||
|
|
||||||
|
outbounds.push(serde_json::json!({
|
||||||
|
"type": "ostp",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": server_host,
|
||||||
|
"port": server_port,
|
||||||
|
"access_key": access_key,
|
||||||
|
"transport": {
|
||||||
|
"type": transport_type
|
||||||
|
},
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": mux_enabled,
|
||||||
|
"sessions": mux_sessions
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
outbounds.push(serde_json::json!({
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}));
|
||||||
|
|
||||||
|
outbounds.push(serde_json::json!({
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
}));
|
||||||
|
|
||||||
|
new_json["outbounds"] = serde_json::Value::Array(outbounds);
|
||||||
|
|
||||||
|
// 4. Routing
|
||||||
|
let mut rules = Vec::new();
|
||||||
|
|
||||||
|
// Migrate exclusions to route to direct
|
||||||
|
if let Some(exclude) = json.get("exclude") {
|
||||||
|
if let Some(domains) = exclude.get("domains") {
|
||||||
|
rules.push(serde_json::json!({
|
||||||
|
"domain_suffix": domains,
|
||||||
|
"outbound": "direct"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if let Some(ips) = exclude.get("ips") {
|
||||||
|
rules.push(serde_json::json!({
|
||||||
|
"ip_cidr": ips,
|
||||||
|
"outbound": "direct"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if let Some(processes) = exclude.get("processes") {
|
||||||
|
rules.push(serde_json::json!({
|
||||||
|
"process_name": processes,
|
||||||
|
"outbound": "direct"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_json["routing"] = serde_json::json!({
|
||||||
|
"rules": rules,
|
||||||
|
"default_outbound": "proxy"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Preserve GUI state
|
||||||
|
if let Some(gui) = json.get("gui") {
|
||||||
|
new_json["gui"] = gui.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
(new_json, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
378
ostp/src/main.rs
378
ostp/src/main.rs
|
|
@ -65,7 +65,7 @@ struct Args {
|
||||||
proxy_env_clear: bool,
|
proxy_env_clear: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
|
fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
let parsed = url::Url::parse(link)
|
let parsed = url::Url::parse(link)
|
||||||
.map_err(|e| anyhow!("Failed to parse share link URL: {e}"))?;
|
.map_err(|e| anyhow!("Failed to parse share link URL: {e}"))?;
|
||||||
|
|
||||||
|
|
@ -98,29 +98,55 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ClientConfig {
|
Ok(serde_json::json!({
|
||||||
server,
|
"version": "0.3.1",
|
||||||
access_key,
|
"log": {
|
||||||
mtu: None,
|
"level": "info"
|
||||||
transport: Some(TransportConfigRaw {
|
},
|
||||||
mode: Some(transport_mode),
|
"inbounds": [
|
||||||
stealth_sni: Some(sni.clone()),
|
{
|
||||||
wss: Some(wss_enabled),
|
"type": "tun",
|
||||||
}),
|
"tag": "tun-in",
|
||||||
socks5_bind: Some("127.0.0.1:1088".to_string()),
|
"auto_route": tun_enabled,
|
||||||
tun: Some(TunConfig {
|
"mtu": 1140
|
||||||
enable: tun_enabled,
|
},
|
||||||
wintun_path: Some("./wintun.dll".to_string()),
|
{
|
||||||
ipv4_address: Some("10.1.0.2/24".to_string()),
|
"type": "local_proxy",
|
||||||
dns: tun_dns,
|
"tag": "socks-in",
|
||||||
kill_switch: Some(false),
|
"protocol": "socks",
|
||||||
}),
|
"listen": "127.0.0.1",
|
||||||
|
"port": 1088
|
||||||
debug: Some(false),
|
}
|
||||||
exclude: None,
|
],
|
||||||
mux: None,
|
"outbounds": [
|
||||||
gui: None,
|
{
|
||||||
})
|
"type": "ostp",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": parsed.host_str().unwrap_or(""),
|
||||||
|
"port": parsed.port().unwrap_or(50000),
|
||||||
|
"access_key": access_key,
|
||||||
|
"transport": {
|
||||||
|
"type": transport_mode
|
||||||
|
},
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": false,
|
||||||
|
"sessions": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routing": {
|
||||||
|
"rules": [],
|
||||||
|
"default_outbound": "proxy"
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_secure_key(format_type: &str) -> String {
|
fn generate_secure_key(format_type: &str) -> String {
|
||||||
|
|
@ -148,7 +174,7 @@ fn parse_outbound_action(value: Option<String>) -> ostp_server::OutboundAction {
|
||||||
#[serde(tag = "mode", rename_all = "lowercase")]
|
#[serde(tag = "mode", rename_all = "lowercase")]
|
||||||
enum AppMode {
|
enum AppMode {
|
||||||
Server(ServerConfig),
|
Server(ServerConfig),
|
||||||
Client(ClientConfig),
|
Client(serde_json::Value),
|
||||||
Relay(RelayServerConfig),
|
Relay(RelayServerConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,8 +204,11 @@ impl UnifiedConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppMode::Client(cfg) => {
|
AppMode::Client(cfg) => {
|
||||||
if cfg.access_key.is_empty() {
|
if let Some(outbounds) = cfg.get("outbounds").and_then(|o| o.as_array()) {
|
||||||
anyhow::bail!("Client configuration must contain an access_key.");
|
let has_proxy = outbounds.iter().any(|o| o.get("type").and_then(|t| t.as_str()) == Some("ostp"));
|
||||||
|
if !has_proxy {
|
||||||
|
anyhow::bail!("Client configuration must contain an ostp outbound proxy.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppMode::Relay(cfg) => {
|
AppMode::Relay(cfg) => {
|
||||||
|
|
@ -222,11 +251,37 @@ impl UserConfig {
|
||||||
pub fn limit(&self) -> Option<u64> {
|
pub fn limit(&self) -> Option<u64> {
|
||||||
match self {
|
match self {
|
||||||
UserConfig::KeyOnly(_) => None,
|
UserConfig::KeyOnly(_) => None,
|
||||||
UserConfig::Detailed { limit_bytes, .. } => limit_bytes.clone(),
|
UserConfig::Detailed { limit_bytes, .. } => *limit_bytes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct OutboundConfig {
|
||||||
|
enabled: bool,
|
||||||
|
protocol: String,
|
||||||
|
address: String,
|
||||||
|
port: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
rules: Vec<OutboundRule>,
|
||||||
|
default_action: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct OutboundRule {
|
||||||
|
domain_suffix: Option<Vec<String>>,
|
||||||
|
ip_cidr: Option<Vec<String>>,
|
||||||
|
protocol: Option<String>,
|
||||||
|
action: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
struct TransportConfigRaw {
|
||||||
|
mode: Option<String>,
|
||||||
|
stealth_sni: Option<String>,
|
||||||
|
wss: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct ServerConfig {
|
struct ServerConfig {
|
||||||
listen: ListenConfig,
|
listen: ListenConfig,
|
||||||
|
|
@ -302,68 +357,7 @@ struct FallbackCfg {
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct ClientConfig {
|
|
||||||
server: String,
|
|
||||||
access_key: String,
|
|
||||||
mtu: Option<usize>,
|
|
||||||
socks5_bind: Option<String>,
|
|
||||||
tun: Option<TunConfig>,
|
|
||||||
debug: Option<bool>,
|
|
||||||
exclude: Option<ExcludeConfig>,
|
|
||||||
mux: Option<MuxConfig>,
|
|
||||||
transport: Option<TransportConfigRaw>,
|
|
||||||
gui: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
struct TransportConfigRaw {
|
|
||||||
mode: Option<String>,
|
|
||||||
stealth_sni: Option<String>,
|
|
||||||
wss: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
struct TunConfig {
|
|
||||||
enable: bool,
|
|
||||||
wintun_path: Option<String>,
|
|
||||||
ipv4_address: Option<String>,
|
|
||||||
dns: Option<String>,
|
|
||||||
kill_switch: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct OutboundConfig {
|
|
||||||
enabled: bool,
|
|
||||||
protocol: String,
|
|
||||||
address: String,
|
|
||||||
port: u16,
|
|
||||||
#[serde(default)]
|
|
||||||
rules: Vec<OutboundRule>,
|
|
||||||
default_action: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct OutboundRule {
|
|
||||||
domain_suffix: Option<Vec<String>>,
|
|
||||||
ip_cidr: Option<Vec<String>>,
|
|
||||||
protocol: Option<String>,
|
|
||||||
action: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct ExcludeConfig {
|
|
||||||
domains: Option<Vec<String>>,
|
|
||||||
ips: Option<Vec<String>>,
|
|
||||||
processes: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct MuxConfig {
|
|
||||||
enabled: Option<bool>,
|
|
||||||
sessions: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
|
@ -679,34 +673,68 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
let _ = key_for_gen;
|
let _ = key_for_gen;
|
||||||
let _ = &sni;
|
let _ = &sni;
|
||||||
|
|
||||||
|
let server_parts: Vec<&str> = server.split(':').collect();
|
||||||
|
let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1");
|
||||||
|
let server_port = server_parts.get(1).unwrap_or(&"50000").parse::<u16>().unwrap_or(50000);
|
||||||
|
|
||||||
|
let socks_parts: Vec<&str> = socks_bind.split(':').collect();
|
||||||
|
let socks_host = socks_parts.get(0).unwrap_or(&"127.0.0.1");
|
||||||
|
let socks_port = socks_parts.get(1).unwrap_or(&"1088").parse::<u16>().unwrap_or(1088);
|
||||||
|
|
||||||
let client_json = serde_json::json!({
|
let client_json = serde_json::json!({
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"log_level": "info",
|
"version": "0.3.1",
|
||||||
"server": server,
|
"log": {
|
||||||
"access_key": access_key,
|
"level": "info"
|
||||||
"socks5_bind": socks_bind,
|
|
||||||
"tun": {
|
|
||||||
"enable": tun_enable,
|
|
||||||
"wintun_path": "./wintun.dll",
|
|
||||||
"ipv4_address": "10.1.0.2/24",
|
|
||||||
"dns": tun_dns,
|
|
||||||
"kill_switch": kill_switch
|
|
||||||
},
|
},
|
||||||
"exclude": {
|
"inbounds": [
|
||||||
"domains": ["localhost", "127.0.0.1"],
|
{
|
||||||
"ips": [],
|
"type": "tun",
|
||||||
"processes": []
|
"tag": "tun-in",
|
||||||
},
|
"auto_route": tun_enable,
|
||||||
"transport": {
|
"mtu": 1140
|
||||||
"mode": transport_mode,
|
},
|
||||||
"stealth_sni": "www.microsoft.com",
|
{
|
||||||
"wss": false
|
"type": "local_proxy",
|
||||||
},
|
"tag": "socks-in",
|
||||||
"mux": {
|
"protocol": "socks",
|
||||||
"enabled": mux_enable,
|
"listen": socks_host,
|
||||||
"sessions": mux_sessions
|
"port": socks_port
|
||||||
},
|
}
|
||||||
"debug": false
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "ostp",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": server_host,
|
||||||
|
"port": server_port,
|
||||||
|
"access_key": access_key,
|
||||||
|
"transport": {
|
||||||
|
"type": transport_mode
|
||||||
|
},
|
||||||
|
"multiplex": {
|
||||||
|
"enabled": mux_enable,
|
||||||
|
"sessions": mux_sessions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routing": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"domain_suffix": ["localhost", "127.0.0.1"],
|
||||||
|
"outbound": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_outbound": "proxy"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let actual_path = wizard_save_config(config_path, &client_json)?;
|
let actual_path = wizard_save_config(config_path, &client_json)?;
|
||||||
|
|
@ -755,6 +783,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
// intentional: step text then daemon call below
|
// intentional: step text then daemon call below
|
||||||
let server_json = serde_json::json!({
|
let server_json = serde_json::json!({
|
||||||
"mode": "server",
|
"mode": "server",
|
||||||
|
"version": "0.3.1",
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"listen": listen,
|
"listen": listen,
|
||||||
"access_keys": access_keys,
|
"access_keys": access_keys,
|
||||||
|
|
@ -867,6 +896,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
let panel_bind = format!("0.0.0.0:{}", panel_port);
|
let panel_bind = format!("0.0.0.0:{}", panel_port);
|
||||||
let server_json = serde_json::json!({
|
let server_json = serde_json::json!({
|
||||||
"mode": "server",
|
"mode": "server",
|
||||||
|
"version": "0.3.1",
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
"listen": listen,
|
"listen": listen,
|
||||||
"access_keys": access_keys,
|
"access_keys": access_keys,
|
||||||
|
|
@ -929,6 +959,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
wizard_step(2, TOTAL, "Saving configuration");
|
wizard_step(2, TOTAL, "Saving configuration");
|
||||||
let relay_json = serde_json::json!({
|
let relay_json = serde_json::json!({
|
||||||
"mode": "relay",
|
"mode": "relay",
|
||||||
|
"version": "0.3.1",
|
||||||
"listen": listen,
|
"listen": listen,
|
||||||
"upstream_tcp": upstream,
|
"upstream_tcp": upstream,
|
||||||
"upstream_udp": upstream,
|
"upstream_udp": upstream,
|
||||||
|
|
@ -1060,9 +1091,15 @@ async fn run_app() -> Result<()> {
|
||||||
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||||
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
|
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
|
||||||
if let AppMode::Client(c) = config.mode {
|
if let AppMode::Client(c) = config.mode {
|
||||||
if let Some(bind) = c.socks5_bind {
|
let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(c);
|
||||||
if let Some(p) = bind.split(':').last().and_then(|s| s.parse::<u16>().ok()) {
|
if let Some(inbounds) = migrated.get("inbounds").and_then(|i| i.as_array()) {
|
||||||
port = p;
|
for inbound in inbounds {
|
||||||
|
if inbound.get("type").and_then(|t| t.as_str()) == Some("local_proxy") {
|
||||||
|
if let Some(p) = inbound.get("port").and_then(|p| p.as_u64()) {
|
||||||
|
port = p as u16;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1150,8 +1187,14 @@ async fn run_app() -> Result<()> {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
if input.trim().eq_ignore_ascii_case("y") {
|
if input.trim().eq_ignore_ascii_case("y") {
|
||||||
if let Some(tun) = &mut client_cfg.tun {
|
if let Some(i_val) = client_cfg.get_mut("inbounds") {
|
||||||
tun.enable = true;
|
if let Some(inbounds) = i_val.as_array_mut() {
|
||||||
|
for inbound in inbounds.iter_mut() {
|
||||||
|
if inbound.get("type").and_then(|t| t.as_str()) == Some("tun") {
|
||||||
|
inbound["auto_route"] = serde_json::json!(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1170,14 +1213,17 @@ async fn run_app() -> Result<()> {
|
||||||
sessions = s;
|
sessions = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if client_cfg.mux.is_none() {
|
if let Some(o_val) = client_cfg.get_mut("outbounds") {
|
||||||
client_cfg.mux = Some(MuxConfig {
|
if let Some(outbounds) = o_val.as_array_mut() {
|
||||||
enabled: Some(true),
|
for outbound in outbounds.iter_mut() {
|
||||||
sessions: Some(sessions),
|
if outbound.get("type").and_then(|t| t.as_str()) == Some("ostp") {
|
||||||
});
|
outbound["multiplex"] = serde_json::json!({
|
||||||
} else if let Some(mux) = &mut client_cfg.mux {
|
"enabled": true,
|
||||||
mux.enabled = Some(true);
|
"sessions": sessions
|
||||||
mux.sessions = Some(sessions);
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1186,7 +1232,7 @@ async fn run_app() -> Result<()> {
|
||||||
input.clear();
|
input.clear();
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
if input.trim().eq_ignore_ascii_case("y") {
|
if input.trim().eq_ignore_ascii_case("y") {
|
||||||
client_cfg.debug = Some(true);
|
client_cfg["log"]["level"] = serde_json::json!("debug");
|
||||||
}
|
}
|
||||||
|
|
||||||
return run_client_directly(client_cfg).await;
|
return run_client_directly(client_cfg).await;
|
||||||
|
|
@ -1226,8 +1272,24 @@ async fn run_app() -> Result<()> {
|
||||||
}
|
}
|
||||||
AppMode::Client(c) => {
|
AppMode::Client(c) => {
|
||||||
println!("{} Config OK: client mode", "[ostp]".green().bold());
|
println!("{} Config OK: client mode", "[ostp]".green().bold());
|
||||||
println!(" Server: {}", c.server.cyan());
|
let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(c.clone());
|
||||||
println!(" Key: {}...", &c.access_key[..8.min(c.access_key.len())].yellow());
|
let mut display_server = "unknown";
|
||||||
|
let mut display_key = "unknown";
|
||||||
|
if let Some(outbounds) = migrated.get("outbounds").and_then(|o| o.as_array()) {
|
||||||
|
for outbound in outbounds {
|
||||||
|
if outbound.get("type").and_then(|t| t.as_str()) == Some("ostp") {
|
||||||
|
if let Some(s) = outbound.get("server").and_then(|s| s.as_str()) {
|
||||||
|
display_server = s;
|
||||||
|
}
|
||||||
|
if let Some(k) = outbound.get("access_key").and_then(|k| k.as_str()) {
|
||||||
|
display_key = k;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(" Server: {}", display_server.cyan());
|
||||||
|
println!(" Key: {}...", &display_key[..8.min(display_key.len())].yellow());
|
||||||
}
|
}
|
||||||
AppMode::Relay(r) => {
|
AppMode::Relay(r) => {
|
||||||
println!("{} Config OK: relay mode", "[ostp]".green().bold());
|
println!("{} Config OK: relay mode", "[ostp]".green().bold());
|
||||||
|
|
@ -1601,46 +1663,26 @@ fn cmd_update() -> Result<()> {
|
||||||
anyhow::bail!("The 'update' command is only supported on Linux/Unix systems.");
|
anyhow::bail!("The 'update' command is only supported on Linux/Unix systems.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
|
async fn run_client_directly(client_cfg: serde_json::Value) -> Result<()> {
|
||||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg);
|
||||||
|
let client_conf: ostp_client::config::ClientConfig = serde_json::from_value(migrated)?;
|
||||||
|
|
||||||
|
let mut is_tun_enabled = false;
|
||||||
|
for inbound in &client_conf.inbounds {
|
||||||
|
if matches!(inbound, ostp_client::config::InboundConfig::Tun { .. }) {
|
||||||
|
is_tun_enabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
||||||
println!("{} Starting client (mode={}, server={})", "[ostp]".cyan().bold(), mode_str.yellow(), client_cfg.server.cyan()); let client_conf = ostp_client::config::ClientConfig {
|
println!("{} Starting client (mode={})", "[ostp]".cyan().bold(), mode_str.yellow());
|
||||||
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
|
||||||
tun_stack: "native".to_string(),
|
|
||||||
debug: client_cfg.debug.unwrap_or(false),
|
|
||||||
ostp: ostp_client::config::OstpConfig {
|
|
||||||
server_addr: client_cfg.server.clone(),
|
|
||||||
local_bind_addr: "0.0.0.0:0".to_string(),
|
|
||||||
access_key: client_cfg.access_key.clone(),
|
|
||||||
handshake_timeout_ms: 5000,
|
|
||||||
io_timeout_ms: 2500,
|
|
||||||
mtu: client_cfg.mtu.unwrap_or(1350),
|
|
||||||
keepalive_interval_sec: 5,
|
|
||||||
},
|
|
||||||
local_proxy: ostp_client::config::LocalProxyConfig {
|
|
||||||
bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
|
|
||||||
connect_timeout_ms: 5000,
|
|
||||||
},
|
|
||||||
exclusions: ostp_client::config::ExclusionConfig {
|
|
||||||
domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
|
||||||
ips: client_cfg.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(),
|
|
||||||
processes: client_cfg.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(),
|
|
||||||
},
|
|
||||||
multiplex: ostp_client::config::MultiplexConfig {
|
|
||||||
enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
|
|
||||||
sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
|
|
||||||
},
|
|
||||||
transport: ostp_client::config::TransportConfig {
|
|
||||||
mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
|
||||||
stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
|
|
||||||
wss: client_cfg.transport.as_ref().and_then(|t| t.wss).unwrap_or(false),
|
|
||||||
},
|
|
||||||
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),
|
|
||||||
kill_switch: client_cfg.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
|
|
||||||
gui: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the client implementation
|
// Run the client implementation
|
||||||
ostp_client::runner::run_client(client_conf).await?;
|
let (_shutdown_tx, rx) = tokio::sync::watch::channel(false);
|
||||||
|
let metrics = std::sync::Arc::new(ostp_client::bridge::BridgeMetrics::default());
|
||||||
|
|
||||||
|
// Launch the core runner directly.
|
||||||
|
ostp_client::runner::run_client_core(client_conf, metrics, rx, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue