From b63979b01462ed6412d6d763d9432d2f675eb921 Mon Sep 17 00:00:00 2001 From: ospab Date: Fri, 15 May 2026 22:17:55 +0300 Subject: [PATCH] feat: add custom DNS server & Exclusions config fields, simplify share link schema, introduce --links server helper --- ostp-client/src/config.rs | 4 ++ ostp-client/src/tunnel/wintun_handler.rs | 19 ++++++--- ostp-gui/src-tauri/src/lib.rs | 2 + ostp-gui/src/index.html | 23 ++++++++++ ostp-gui/src/main.js | 49 +++++++++++++++------- ostp-gui/src/styles.css | 35 +++++++++++++++- ostp/src/main.rs | 53 +++++++++++++++--------- 7 files changed, 143 insertions(+), 42 deletions(-) diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index e061120..d21d51e 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -17,6 +17,7 @@ pub struct ClientConfig { pub exclusions: ExclusionConfig, #[serde(default)] pub multiplex: MultiplexConfig, + pub dns_server: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -101,6 +102,7 @@ impl Default for ClientConfig { turn: TurnConfig::default(), exclusions: ExclusionConfig::default(), multiplex: MultiplexConfig::default(), + dns_server: None, } } } @@ -132,6 +134,7 @@ struct RawUnifiedConfig { #[derive(Debug, Deserialize)] struct RawTunSection { enable: Option, + dns: Option, } #[derive(Debug, Deserialize)] @@ -198,6 +201,7 @@ impl ClientConfig { enabled: mux.enabled.unwrap_or(false), sessions: mux.sessions.unwrap_or(1), }, + dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()), }) } } diff --git a/ostp-client/src/tunnel/wintun_handler.rs b/ostp-client/src/tunnel/wintun_handler.rs index 992dc92..a4cd8c3 100644 --- a/ostp-client/src/tunnel/wintun_handler.rs +++ b/ostp-client/src/tunnel/wintun_handler.rs @@ -126,22 +126,29 @@ pub async fn run_wintun_tunnel( child: None, // Will set below }; - // 5. Once tun2socks creates the interface, apply network settings (IP, metric) + // 5. Once tun2socks creates the interface, apply network settings (IP, metric, MTU) tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; if debug { println!("[ostp-client] Applying network configurations onto 'ostp_tun' interface..."); } - // We omit setting dnsservers on the TUN interface entirely. This allows Windows to natively fallback - // to the physical interface DNS servers, which are physically routed and work flawlessly. - let net_setup = "\ + let mut net_setup = String::from("\ netsh interface ipv4 set address name=\"ostp_tun\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\ netsh interface ipv4 set subinterface \"ostp_tun\" mtu=1300 store=persistent\n\ - netsh interface ipv4 set interface name=\"ostp_tun\" metric=5\n"; + netsh interface ipv4 set interface name=\"ostp_tun\" metric=5\n"); + + if let Some(ref dns) = config.dns_server { + if !dns.is_empty() { + if debug { + println!("[ostp-client] Applying custom DNS server: {}", dns); + } + net_setup.push_str(&format!("netsh interface ipv4 set dnsservers name=\"ostp_tun\" static {} primary\n", dns)); + } + } let _ = Command::new("powershell") - .args(["-Command", net_setup]) + .args(["-Command", &net_setup]) .output()?; println!("[client] TUN Tunnel established, internet traffic is now routing through OSTP."); diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 0a9fa1f..1dad344 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -39,6 +39,7 @@ struct TunConfig { enable: bool, wintun_path: Option, ipv4_address: Option, + dns: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -224,6 +225,7 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result 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), }, + dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()), }; let metrics = Arc::new(BridgeMetrics { diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 9ef7ed8..e5b86e8 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -98,6 +98,11 @@ +
+ + +
+
TUN Tunnel Mode @@ -119,6 +124,24 @@
+ + +
Exclusions (one per line)
+ +
+ + +
+ +
+ + +
+ +
+ + +
diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index d551212..f66eba7 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -28,9 +28,15 @@ const btnImportUrl = document.getElementById('btn-import-url'); const inServer = document.getElementById('in-server'); const inKey = document.getElementById('in-key'); const inSocks = document.getElementById('in-socks'); +const inDns = document.getElementById('in-dns'); const inTunMode = document.getElementById('in-tun-mode'); const inDebug = document.getElementById('in-debug'); +// Exclusions Textareas +const inExDomains = document.getElementById('in-ex-domains'); +const inExIps = document.getElementById('in-ex-ips'); +const inExProcesses = document.getElementById('in-ex-processes'); + // Utils function formatBytes(bytes) { if (bytes === 0) return '0.0 B'; @@ -110,8 +116,6 @@ async function handleToggleConnect() { setUIState('disconnected'); } } catch (err) { - // If the error tells that app exited (due to Admin elevation relaunching), don't show an alert. - // Elevation relaunching closes current app instance silently. console.error(err); setUIState('disconnected'); } @@ -183,7 +187,6 @@ async function loadConfigIntoFields() { const rawStr = await invoke('get_config'); rawConfigObj = JSON.parse(rawStr); - // Determine if Server mode or Client mode is active const isClient = rawConfigObj.mode === 'client'; const clientConf = isClient ? rawConfigObj : null; @@ -195,7 +198,14 @@ async function loadConfigIntoFields() { const tunEnabled = clientConf.tun && clientConf.tun.enable; inTunMode.checked = !!tunEnabled; + inDns.value = (clientConf.tun && clientConf.tun.dns) || ''; inDebug.checked = !!clientConf.debug; + + // Load exclusions (arrays to multiline string) + const exc = clientConf.exclude || {}; + inExDomains.value = (exc.domains || []).join('\n'); + inExIps.value = (exc.ips || []).join('\n'); + inExProcesses.value = (exc.processes || []).join('\n'); } else { alert('Loaded configuration is for OSTP Server. Please adjust manually.'); } @@ -204,10 +214,15 @@ async function loadConfigIntoFields() { } } +function parseTextAreaToArray(val) { + return val.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); +} + async function handleSaveConfig() { if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' }; - // Enforce client settings format rawConfigObj.mode = 'client'; rawConfigObj.server = inServer.value.trim(); rawConfigObj.access_key = inKey.value.trim(); @@ -220,8 +235,19 @@ async function handleSaveConfig() { }; } rawConfigObj.tun.enable = inTunMode.checked; + + const dnsVal = inDns.value.trim(); + rawConfigObj.tun.dns = dnsVal ? dnsVal : null; + rawConfigObj.debug = inDebug.checked; + // Save Exclusions + rawConfigObj.exclude = { + domains: parseTextAreaToArray(inExDomains.value), + ips: parseTextAreaToArray(inExIps.value), + processes: parseTextAreaToArray(inExProcesses.value) + }; + // Validation if (!rawConfigObj.server) { alert('Server Address is required!'); @@ -244,7 +270,7 @@ async function handleSaveConfig() { } } -// OSTP URI Sharing Parser +// OSTP URI Sharing Parser (Simplified: only extract HOST & KEY) function handleImportUrl() { const urlStr = inImportUrl.value.trim(); if (!urlStr) return; @@ -253,27 +279,21 @@ function handleImportUrl() { if (!urlStr.startsWith('ostp://')) { throw new Error('Link must start with ostp://'); } - // Standard URL parsing const url = new URL(urlStr); const accessKey = decodeURIComponent(url.username); - const serverHost = url.host; // Includes hostname:port - const useTun = url.searchParams.get('tun') === '1' || url.searchParams.get('tun') === 'true'; - const socks5 = url.searchParams.get('socks5'); + const serverHost = url.host; if (!accessKey || !serverHost) { throw new Error('Incomplete parameters: missing key or server address.'); } - // Update fields + // Update primary connection fields inServer.value = serverHost; inKey.value = accessKey; - inTunMode.checked = useTun; - if (socks5) inSocks.value = socks5; - inImportUrl.value = ''; // Clear import input + inImportUrl.value = ''; - // Small animation or visual confirm inImportUrl.placeholder = 'Import successful!'; setTimeout(() => { inImportUrl.placeholder = 'Paste ostp:// share link here...'; }, 2000); @@ -299,7 +319,6 @@ window.addEventListener('DOMContentLoaded', async () => { if (e.key === 'Enter') handleImportUrl(); }); - // Check current status on startup try { const isAlive = await invoke('get_tunnel_status'); if (isAlive) { diff --git a/ostp-gui/src/styles.css b/ostp-gui/src/styles.css index 4544124..8c996b3 100644 --- a/ostp-gui/src/styles.css +++ b/ostp-gui/src/styles.css @@ -474,7 +474,8 @@ h2 { } .form-group input[type="text"], -.form-group input[type="password"] { +.form-group input[type="password"], +.form-group textarea { width: 100%; background: rgba(0, 0, 0, 0.15); border: 1px solid var(--glass-border); @@ -484,13 +485,43 @@ h2 { font-size: 0.9rem; transition: all 0.3s; outline: none; + font-family: inherit; } -.form-group input:focus { +.form-group textarea { + resize: vertical; + min-height: 56px; + max-height: 120px; + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 0.8rem; + line-height: 1.4; +} + +.form-group input:focus, +.form-group textarea:focus { border-color: var(--accent-primary); background: rgba(0, 0, 0, 0.25); } +.section-divider { + margin-top: 8px; + padding-top: 15px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.3px; + color: var(--text-primary); + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.divider-hint { + font-size: 0.65rem; + color: var(--text-secondary); + font-weight: 400; +} + .label-stack { display: flex; flex-direction: column; diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 3140b89..f94915f 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -27,7 +27,11 @@ struct Args { #[arg(short = 'c', long, default_value_t = 1)] count: usize, - /// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT/?tun=1) to run instantly + /// Output ready-to-use client sharing links (ostp://...) from the server configuration + #[arg(long)] + links: bool, + + /// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT) to run instantly url: Option, } @@ -48,26 +52,15 @@ fn parse_ostp_link(link: &str) -> Result { let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?; let server = format!("{host}:{port}"); - let mut use_tun = false; - let mut socks5 = None; - - for (k, v) in parsed.query_pairs() { - if k == "tun" && (v == "1" || v.eq_ignore_ascii_case("true")) { - use_tun = true; - } - if k == "socks5" { - socks5 = Some(v.to_string()); - } - } - Ok(ClientConfig { server, access_key, - socks5_bind: socks5, + socks5_bind: Some("127.0.0.1:1088".to_string()), // Fallback to standard SOCKS5 port tun: Some(TunConfig { - enable: use_tun, + enable: false, // Default to proxy, configurable via settings GUI wintun_path: Some("./wintun.dll".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()), + dns: None, }), turn: None, debug: Some(false), @@ -131,11 +124,12 @@ struct ClientConfig { mux: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] struct TunConfig { enable: bool, wintun_path: Option, ipv4_address: Option, + dns: Option, } #[derive(Debug, Deserialize, Serialize)] @@ -246,6 +240,7 @@ async fn run_app() -> Result<()> { enable: false, wintun_path: Some("./wintun.dll".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()), + dns: None, }), turn: None, debug: Some(false), @@ -267,9 +262,8 @@ async fn run_app() -> Result<()> { if is_server { if let AppMode::Server(s) = dummy.mode { let key = &s.access_keys[0]; - println!("\n>>> Handy Client Share Links for your users:"); - println!(" TUN mode: ostp://{}@:50000/?tun=1", key); - println!(" PROXY mode: ostp://{}@:50000/?tun=0", key); + println!("\n>>> Handy Client Share Link for your users:"); + println!(" ostp://{}@:50000", key); } } return Ok(()); @@ -293,6 +287,26 @@ async fn run_app() -> Result<()> { let config: UnifiedConfig = serde_json::from_str(&config_content) .map_err(|e| anyhow!("Failed to parse config: {}", e))?; + if args.links { + match config.mode { + AppMode::Server(server_cfg) => { + let listen = server_cfg.listen.clone(); + let parts: Vec<&str> = listen.split(':').collect(); + let port = parts.get(1).unwrap_or(&"50000"); + let host = if parts[0] == "0.0.0.0" { "" } else { parts[0] }; + + println!("\n>>> Ready-to-use OSTP client share links from {:?}:", args.config); + for (idx, key) in server_cfg.access_keys.iter().enumerate() { + println!(" [{}] ostp://{}@{}:{}", idx + 1, key, host, port); + } + return Ok(()); + } + AppMode::Client(_) => { + anyhow::bail!("The configuration file is in Client mode. The --links flag can only extract keys from a Server configuration."); + } + } + } + match config.mode { AppMode::Server(server_cfg) => { println!("[OSTP Core] Starting in SERVER mode on {}", server_cfg.listen); @@ -370,6 +384,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { 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), }, + dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()), }; // Run the client implementation ostp_client::runner::run_client(client_conf).await?;