mirror of https://github.com/ospab/ostp.git
feat: add custom DNS server & Exclusions config fields, simplify share link schema, introduce --links server helper
This commit is contained in:
parent
067ee758cd
commit
b63979b014
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
Loading…
Reference in New Issue