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,
#[serde(default)]
pub multiplex: MultiplexConfig,
pub dns_server: Option<String>,
}
#[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<bool>,
dns: Option<String>,
}
#[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()),
})
}
}

View File

@ -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.");

View File

@ -39,6 +39,7 @@ struct TunConfig {
enable: bool,
wintun_path: Option<String>,
ipv4_address: Option<String>,
dns: Option<String>,
}
#[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),
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 {

View File

@ -98,6 +98,11 @@
<input type="text" id="in-socks" placeholder="127.0.0.1:1088" />
</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="label-stack">
<span class="toggle-label">TUN Tunnel Mode</span>
@ -119,6 +124,24 @@
<span class="slider round"></span>
</label>
</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 class="actions-container">

View File

@ -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) {

View File

@ -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;

View File

@ -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<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 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<MuxConfig>,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone)]
struct TunConfig {
enable: bool,
wintun_path: Option<String>,
ipv4_address: Option<String>,
dns: Option<String>,
}
#[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://{}@<YOUR_SERVER_PUBLIC_IP>:50000/?tun=1", key);
println!(" PROXY mode: ostp://{}@<YOUR_SERVER_PUBLIC_IP>:50000/?tun=0", key);
println!("\n>>> Handy Client Share Link for your users:");
println!(" ostp://{}@<YOUR_SERVER_PUBLIC_IP>: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" { "<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 {
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?;