feat: add custom DNS server & Exclusions config fields, simplify share link schema, introduce --links server helper

This commit is contained in:
ospab 2026-05-15 22:17:55 +03:00
parent 067ee758cd
commit b63979b014
7 changed files with 143 additions and 42 deletions

View File

@ -17,6 +17,7 @@ pub struct ClientConfig {
pub exclusions: ExclusionConfig, pub exclusions: ExclusionConfig,
#[serde(default)] #[serde(default)]
pub multiplex: MultiplexConfig, pub multiplex: MultiplexConfig,
pub dns_server: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -101,6 +102,7 @@ impl Default for ClientConfig {
turn: TurnConfig::default(), turn: TurnConfig::default(),
exclusions: ExclusionConfig::default(), exclusions: ExclusionConfig::default(),
multiplex: MultiplexConfig::default(), multiplex: MultiplexConfig::default(),
dns_server: None,
} }
} }
} }
@ -132,6 +134,7 @@ struct RawUnifiedConfig {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RawTunSection { struct RawTunSection {
enable: Option<bool>, enable: Option<bool>,
dns: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -198,6 +201,7 @@ impl ClientConfig {
enabled: mux.enabled.unwrap_or(false), enabled: mux.enabled.unwrap_or(false),
sessions: mux.sessions.unwrap_or(1), sessions: mux.sessions.unwrap_or(1),
}, },
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
}) })
} }
} }

View File

@ -126,22 +126,29 @@ pub async fn run_wintun_tunnel(
child: None, // Will set below 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; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
if debug { if debug {
println!("[ostp-client] Applying network configurations onto 'ostp_tun' interface..."); 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 let mut net_setup = String::from("\
// to the physical interface DNS servers, which are physically routed and work flawlessly.
let net_setup = "\
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 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 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") let _ = Command::new("powershell")
.args(["-Command", net_setup]) .args(["-Command", &net_setup])
.output()?; .output()?;
println!("[client] TUN Tunnel established, internet traffic is now routing through OSTP."); println!("[client] TUN Tunnel established, internet traffic is now routing through OSTP.");

View File

@ -39,6 +39,7 @@ struct TunConfig {
enable: bool, enable: bool,
wintun_path: Option<String>, wintun_path: Option<String>,
ipv4_address: Option<String>, ipv4_address: Option<String>,
dns: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -224,6 +225,7 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false), 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), 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 { let metrics = Arc::new(BridgeMetrics {

View File

@ -98,6 +98,11 @@
<input type="text" id="in-socks" placeholder="127.0.0.1:1088" /> <input type="text" id="in-socks" placeholder="127.0.0.1:1088" />
</div> </div>
<div class="form-group">
<label for="in-dns">Custom DNS Server</label>
<input type="text" id="in-dns" placeholder="8.8.8.8 (Optional)" />
</div>
<div class="form-group row-align"> <div class="form-group row-align">
<div class="label-stack"> <div class="label-stack">
<span class="toggle-label">TUN Tunnel Mode</span> <span class="toggle-label">TUN Tunnel Mode</span>
@ -119,6 +124,24 @@
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
</div> </div>
<!-- Exclusions Section Divider -->
<div class="section-divider">Exclusions <span class="divider-hint">(one per line)</span></div>
<div class="form-group">
<label for="in-ex-domains">Bypass Domains</label>
<textarea id="in-ex-domains" placeholder="example.com" rows="2"></textarea>
</div>
<div class="form-group">
<label for="in-ex-ips">Bypass IP Ranges</label>
<textarea id="in-ex-ips" placeholder="192.168.1.0/24" rows="2"></textarea>
</div>
<div class="form-group">
<label for="in-ex-processes">Bypass Processes (Proxy Mode)</label>
<textarea id="in-ex-processes" placeholder="chrome.exe" rows="2"></textarea>
</div>
</div> </div>
<div class="actions-container"> <div class="actions-container">

View File

@ -28,9 +28,15 @@ const btnImportUrl = document.getElementById('btn-import-url');
const inServer = document.getElementById('in-server'); const inServer = document.getElementById('in-server');
const inKey = document.getElementById('in-key'); const inKey = document.getElementById('in-key');
const inSocks = document.getElementById('in-socks'); const inSocks = document.getElementById('in-socks');
const inDns = document.getElementById('in-dns');
const inTunMode = document.getElementById('in-tun-mode'); const inTunMode = document.getElementById('in-tun-mode');
const inDebug = document.getElementById('in-debug'); 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 // Utils
function formatBytes(bytes) { function formatBytes(bytes) {
if (bytes === 0) return '0.0 B'; if (bytes === 0) return '0.0 B';
@ -110,8 +116,6 @@ async function handleToggleConnect() {
setUIState('disconnected'); setUIState('disconnected');
} }
} catch (err) { } 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); console.error(err);
setUIState('disconnected'); setUIState('disconnected');
} }
@ -183,7 +187,6 @@ async function loadConfigIntoFields() {
const rawStr = await invoke('get_config'); const rawStr = await invoke('get_config');
rawConfigObj = JSON.parse(rawStr); rawConfigObj = JSON.parse(rawStr);
// Determine if Server mode or Client mode is active
const isClient = rawConfigObj.mode === 'client'; const isClient = rawConfigObj.mode === 'client';
const clientConf = isClient ? rawConfigObj : null; const clientConf = isClient ? rawConfigObj : null;
@ -195,7 +198,14 @@ async function loadConfigIntoFields() {
const tunEnabled = clientConf.tun && clientConf.tun.enable; const tunEnabled = clientConf.tun && clientConf.tun.enable;
inTunMode.checked = !!tunEnabled; inTunMode.checked = !!tunEnabled;
inDns.value = (clientConf.tun && clientConf.tun.dns) || '';
inDebug.checked = !!clientConf.debug; 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 { } else {
alert('Loaded configuration is for OSTP Server. Please adjust manually.'); 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() { async function handleSaveConfig() {
if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' }; if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' };
// Enforce client settings format
rawConfigObj.mode = 'client'; rawConfigObj.mode = 'client';
rawConfigObj.server = inServer.value.trim(); rawConfigObj.server = inServer.value.trim();
rawConfigObj.access_key = inKey.value.trim(); rawConfigObj.access_key = inKey.value.trim();
@ -220,8 +235,19 @@ async function handleSaveConfig() {
}; };
} }
rawConfigObj.tun.enable = inTunMode.checked; rawConfigObj.tun.enable = inTunMode.checked;
const dnsVal = inDns.value.trim();
rawConfigObj.tun.dns = dnsVal ? dnsVal : null;
rawConfigObj.debug = inDebug.checked; rawConfigObj.debug = inDebug.checked;
// Save Exclusions
rawConfigObj.exclude = {
domains: parseTextAreaToArray(inExDomains.value),
ips: parseTextAreaToArray(inExIps.value),
processes: parseTextAreaToArray(inExProcesses.value)
};
// Validation // Validation
if (!rawConfigObj.server) { if (!rawConfigObj.server) {
alert('Server Address is required!'); 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() { function handleImportUrl() {
const urlStr = inImportUrl.value.trim(); const urlStr = inImportUrl.value.trim();
if (!urlStr) return; if (!urlStr) return;
@ -253,27 +279,21 @@ function handleImportUrl() {
if (!urlStr.startsWith('ostp://')) { if (!urlStr.startsWith('ostp://')) {
throw new Error('Link must start with ostp://'); throw new Error('Link must start with ostp://');
} }
// Standard URL parsing
const url = new URL(urlStr); const url = new URL(urlStr);
const accessKey = decodeURIComponent(url.username); const accessKey = decodeURIComponent(url.username);
const serverHost = url.host; // Includes hostname:port const serverHost = url.host;
const useTun = url.searchParams.get('tun') === '1' || url.searchParams.get('tun') === 'true';
const socks5 = url.searchParams.get('socks5');
if (!accessKey || !serverHost) { if (!accessKey || !serverHost) {
throw new Error('Incomplete parameters: missing key or server address.'); throw new Error('Incomplete parameters: missing key or server address.');
} }
// Update fields // Update primary connection fields
inServer.value = serverHost; inServer.value = serverHost;
inKey.value = accessKey; 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!'; inImportUrl.placeholder = 'Import successful!';
setTimeout(() => { inImportUrl.placeholder = 'Paste ostp:// share link here...'; }, 2000); setTimeout(() => { inImportUrl.placeholder = 'Paste ostp:// share link here...'; }, 2000);
@ -299,7 +319,6 @@ window.addEventListener('DOMContentLoaded', async () => {
if (e.key === 'Enter') handleImportUrl(); if (e.key === 'Enter') handleImportUrl();
}); });
// Check current status on startup
try { try {
const isAlive = await invoke('get_tunnel_status'); const isAlive = await invoke('get_tunnel_status');
if (isAlive) { if (isAlive) {

View File

@ -474,7 +474,8 @@ h2 {
} }
.form-group input[type="text"], .form-group input[type="text"],
.form-group input[type="password"] { .form-group input[type="password"],
.form-group textarea {
width: 100%; width: 100%;
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.15);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
@ -484,13 +485,43 @@ h2 {
font-size: 0.9rem; font-size: 0.9rem;
transition: all 0.3s; transition: all 0.3s;
outline: none; 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); border-color: var(--accent-primary);
background: rgba(0, 0, 0, 0.25); 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 { .label-stack {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -27,7 +27,11 @@ struct Args {
#[arg(short = 'c', long, default_value_t = 1)] #[arg(short = 'c', long, default_value_t = 1)]
count: usize, 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<String>, url: Option<String>,
} }
@ -48,26 +52,15 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?; let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
let server = format!("{host}:{port}"); 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 { Ok(ClientConfig {
server, server,
access_key, access_key,
socks5_bind: socks5, socks5_bind: Some("127.0.0.1:1088".to_string()), // Fallback to standard SOCKS5 port
tun: Some(TunConfig { tun: Some(TunConfig {
enable: use_tun, enable: false, // Default to proxy, configurable via settings GUI
wintun_path: Some("./wintun.dll".to_string()), wintun_path: Some("./wintun.dll".to_string()),
ipv4_address: Some("10.1.0.2/24".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()),
dns: None,
}), }),
turn: None, turn: None,
debug: Some(false), debug: Some(false),
@ -131,11 +124,12 @@ struct ClientConfig {
mux: Option<MuxConfig>, mux: Option<MuxConfig>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct TunConfig { struct TunConfig {
enable: bool, enable: bool,
wintun_path: Option<String>, wintun_path: Option<String>,
ipv4_address: Option<String>, ipv4_address: Option<String>,
dns: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -246,6 +240,7 @@ async fn run_app() -> Result<()> {
enable: false, enable: false,
wintun_path: Some("./wintun.dll".to_string()), wintun_path: Some("./wintun.dll".to_string()),
ipv4_address: Some("10.1.0.2/24".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()),
dns: None,
}), }),
turn: None, turn: None,
debug: Some(false), debug: Some(false),
@ -267,9 +262,8 @@ async fn run_app() -> Result<()> {
if is_server { if is_server {
if let AppMode::Server(s) = dummy.mode { if let AppMode::Server(s) = dummy.mode {
let key = &s.access_keys[0]; let key = &s.access_keys[0];
println!("\n>>> Handy Client Share Links for your users:"); println!("\n>>> Handy Client Share Link for your users:");
println!(" TUN mode: ostp://{}@<YOUR_SERVER_PUBLIC_IP>:50000/?tun=1", key); println!(" ostp://{}@<YOUR_SERVER_PUBLIC_IP>:50000", key);
println!(" PROXY mode: ostp://{}@<YOUR_SERVER_PUBLIC_IP>:50000/?tun=0", key);
} }
} }
return Ok(()); return Ok(());
@ -293,6 +287,26 @@ async fn run_app() -> Result<()> {
let config: UnifiedConfig = serde_json::from_str(&config_content) let config: UnifiedConfig = serde_json::from_str(&config_content)
.map_err(|e| anyhow!("Failed to parse config: {}", e))?; .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" { "<YOUR_SERVER_PUBLIC_IP>" } 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 { match config.mode {
AppMode::Server(server_cfg) => { AppMode::Server(server_cfg) => {
println!("[OSTP Core] Starting in SERVER mode on {}", server_cfg.listen); 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), 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), 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 // Run the client implementation
ostp_client::runner::run_client(client_conf).await?; ostp_client::runner::run_client(client_conf).await?;