mirror of https://github.com/ospab/ostp.git
feat: Built-in DNS Server with AdBlock and DoH proxy
This commit is contained in:
parent
ba1a5cd16c
commit
cea8ebaa5c
|
|
@ -559,7 +559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1455,6 +1455,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"simple-dns",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
|
@ -2047,6 +2048,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple-dns"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -2709,7 +2719,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||||
|
pub async fn run_native_tunnel(
|
||||||
|
config: crate::config::ClientConfig,
|
||||||
|
mut shutdown: watch::Receiver<bool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use std::process::Command;
|
||||||
|
use netstack_smoltcp::StackBuilder;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use futures::{StreamExt, SinkExt};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
let debug = config.debug;
|
||||||
|
tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)...");
|
||||||
|
|
||||||
|
let server_ip = config.ostp.server_addr.to_socket_addrs()
|
||||||
|
.map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))?
|
||||||
|
.next()
|
||||||
|
.map(|addr| addr.ip())
|
||||||
|
.ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?;
|
||||||
|
|
||||||
|
let server_ip_str = server_ip.to_string();
|
||||||
|
|
||||||
|
let mut tun_cfg = tun::Configuration::default();
|
||||||
|
tun_cfg.tun_name("ostp_tun")
|
||||||
|
.address((10, 1, 0, 2))
|
||||||
|
.netmask((255, 255, 255, 0))
|
||||||
|
.destination((10, 1, 0, 1))
|
||||||
|
.mtu(config.ostp.mtu as u16)
|
||||||
|
.up();
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
tun_cfg.platform_config(|config| {
|
||||||
|
config.packet_information(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dev = tun::create(&tun_cfg)
|
||||||
|
.map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
||||||
|
let dev = tun::AsyncDevice::new(dev)
|
||||||
|
.map_err(|e| anyhow!("Failed to make TUN device async: {}", e))?;
|
||||||
|
|
||||||
|
tracing::info!("TUN device created natively.");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
let setup_script = format!(
|
||||||
|
"$remote_ip = '{}'\n\
|
||||||
|
$exe_path = '{}'\n\
|
||||||
|
$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {{ $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' }} | Sort-Object RouteMetric | Select-Object -First 1\n\
|
||||||
|
if ($route) {{\n\
|
||||||
|
$gw = $route.NextHop\n\
|
||||||
|
$ifIndex = $route.InterfaceIndex\n\
|
||||||
|
New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||||
|
$dns_ips = Get-DnsClientServerAddress -InterfaceIndex $ifIndex | Select-Object -ExpandProperty ServerAddresses\n\
|
||||||
|
foreach ($dns in $dns_ips) {{\n\
|
||||||
|
if ($dns -match '^\\d+\\.\\d+\\.\\d+\\.\\d+$') {{\n\
|
||||||
|
New-NetRoute -DestinationPrefix \"$dns/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||||
|
}}\n\
|
||||||
|
}}\n\
|
||||||
|
New-NetRoute -DestinationPrefix \"1.1.1.1/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||||
|
}}\n\
|
||||||
|
New-NetFirewallRule -DisplayName 'OSTP Tunnel In' -Direction Inbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\
|
||||||
|
New-NetFirewallRule -DisplayName 'OSTP Tunnel Out' -Direction Outbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\
|
||||||
|
netsh interface ipv4 set interface name=\"ostp_tun\" metric=1\n\
|
||||||
|
New-NetRoute -DestinationPrefix '0.0.0.0/0' -InterfaceAlias 'ostp_tun' -NextHop '10.1.0.1' -RouteMetric 1 -ErrorAction SilentlyContinue\n",
|
||||||
|
server_ip_str, current_exe
|
||||||
|
);
|
||||||
|
let _ = Command::new("powershell")
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.args(["-NoProfile", "-Command", &setup_script])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if let Some(ref dns) = config.dns_server {
|
||||||
|
if !dns.is_empty() {
|
||||||
|
let net_setup = format!("netsh interface ipv4 set dnsservers name=\"ostp_tun\" static {} primary\n", dns);
|
||||||
|
let _ = Command::new("powershell")
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.args(["-NoProfile", "-Command", &net_setup])
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Add default route to tun, bypassing server IP
|
||||||
|
let _ = Command::new("ip").args(["route", "add", &format!("{}/32", server_ip_str), "via", "10.1.0.1"]).output();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (stack, tcp_runner, _udp_socket, tcp_listener) = StackBuilder::default()
|
||||||
|
.enable_tcp(true)
|
||||||
|
.enable_udp(true)
|
||||||
|
.mtu(config.ostp.mtu)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut runner_task = tokio::spawn(async move {
|
||||||
|
if let Some(mut runner) = tcp_runner {
|
||||||
|
let _ = runner.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let (mut stack_sink, mut stack_stream) = stack.split();
|
||||||
|
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
|
||||||
|
|
||||||
|
let mut tun_to_stack = tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; 65536];
|
||||||
|
loop {
|
||||||
|
match tun_read.read(&mut buf).await {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
let frame = buf[..n].to_vec();
|
||||||
|
if stack_sink.send(frame).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut stack_to_tun = tokio::spawn(async move {
|
||||||
|
while let Some(Ok(frame)) = stack_stream.next().await {
|
||||||
|
if tun_write.write_all(frame.as_slice()).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let proxy_addr = config.local_proxy.bind_addr.clone();
|
||||||
|
let mut tcp_accept_task = tokio::spawn(async move {
|
||||||
|
if let Some(mut listener) = tcp_listener {
|
||||||
|
while let Some((mut stream, _local, remote)) = listener.next().await {
|
||||||
|
let proxy_addr = proxy_addr.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if debug { tracing::info!("Native TUN intercepted TCP to {}", remote); }
|
||||||
|
if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await {
|
||||||
|
// SOCKS5 bypass handshake locally (loopback)
|
||||||
|
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; }
|
||||||
|
|
||||||
|
let ip = remote.ip();
|
||||||
|
let port = remote.port();
|
||||||
|
let mut req = vec![5, 1, 0];
|
||||||
|
match ip {
|
||||||
|
std::net::IpAddr::V4(v4) => {
|
||||||
|
req.push(1);
|
||||||
|
req.extend_from_slice(&v4.octets());
|
||||||
|
}
|
||||||
|
std::net::IpAddr::V6(v6) => {
|
||||||
|
req.push(4);
|
||||||
|
req.extend_from_slice(&v6.octets());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.extend_from_slice(&port.to_be_bytes());
|
||||||
|
if socks.write_all(&req).await.is_err() { return; }
|
||||||
|
|
||||||
|
let mut rep = [0u8; 10];
|
||||||
|
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
|
||||||
|
|
||||||
|
let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::info!("NATIVE TUN tunnel active.");
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown.changed() => {}
|
||||||
|
_ = &mut runner_task => {}
|
||||||
|
_ = &mut tun_to_stack => {}
|
||||||
|
_ = &mut stack_to_tun => {}
|
||||||
|
_ = &mut tcp_accept_task => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Deactivating NATIVE TUN tunnel...");
|
||||||
|
// Cleanup routes
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
let cleanup_script = format!(
|
||||||
|
"$remote_ip = '{}'\n\
|
||||||
|
Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\
|
||||||
|
Remove-NetRoute -DestinationPrefix \"1.1.1.1/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\
|
||||||
|
Remove-NetFirewallRule -DisplayName 'OSTP Tunnel*' -ErrorAction SilentlyContinue\n\
|
||||||
|
netsh interface ipv4 set dnsservers name=\"ostp_tun\" source=dhcp 2>$null\n",
|
||||||
|
server_ip_str
|
||||||
|
);
|
||||||
|
let _ = Command::new("powershell")
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.args(["-NoProfile", "-Command", &cleanup_script])
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
|
pub async fn run_native_tunnel(
|
||||||
|
_config: crate::config::ClientConfig,
|
||||||
|
_shutdown: watch::Receiver<bool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Err(anyhow!("Native TUN tunnel is only supported on Windows/Linux currently"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub async fn run_native_tunnel_from_fd(
|
||||||
|
config: crate::config::ClientConfig,
|
||||||
|
mut shutdown: watch::Receiver<bool>,
|
||||||
|
fd: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
use netstack_smoltcp::StackBuilder;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use futures::{StreamExt, SinkExt};
|
||||||
|
use std::os::unix::io::{FromRawFd, AsRawFd};
|
||||||
|
|
||||||
|
let debug = config.debug;
|
||||||
|
tracing::info!("Initializing NATIVE TUN tunnel on Android (FD {})", fd);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||||
|
if flags >= 0 {
|
||||||
|
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||||
|
let tun_stream = tokio::io::unix::AsyncFd::new(file)?;
|
||||||
|
|
||||||
|
let (stack, tcp_runner, _udp_socket, tcp_listener) = StackBuilder::default()
|
||||||
|
.enable_tcp(true)
|
||||||
|
.enable_udp(true)
|
||||||
|
.mtu(config.ostp.mtu)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut runner_task = tokio::spawn(async move {
|
||||||
|
if let Some(mut runner) = tcp_runner {
|
||||||
|
let _ = runner.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let (mut stack_sink, mut stack_stream) = stack.split();
|
||||||
|
|
||||||
|
let mut tun_to_stack = tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; 65536];
|
||||||
|
loop {
|
||||||
|
let mut guard = match tun_stream.readable().await {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
let n = match guard.try_io(|inner| {
|
||||||
|
let res = unsafe { libc::read(inner.as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||||
|
if res < 0 {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
if err.kind() == std::io::ErrorKind::WouldBlock {
|
||||||
|
Err(err)
|
||||||
|
} else {
|
||||||
|
Ok(res) // Return error as success to break gracefully
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(Ok(n)) if n > 0 => n as usize,
|
||||||
|
Ok(Ok(_)) => break, // EOF or Error
|
||||||
|
Ok(Err(_)) => continue, // Should not happen with try_io
|
||||||
|
Err(_would_block) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame = buf[..n].to_vec();
|
||||||
|
if stack_sink.send(frame).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let write_fd = unsafe { libc::dup(fd) };
|
||||||
|
if write_fd < 0 {
|
||||||
|
return Err(anyhow!("Failed to dup tun fd"));
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let flags = libc::fcntl(write_fd, libc::F_GETFL);
|
||||||
|
if flags >= 0 {
|
||||||
|
libc::fcntl(write_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let write_file = unsafe { std::fs::File::from_raw_fd(write_fd) };
|
||||||
|
let tun_write_stream = tokio::io::unix::AsyncFd::new(write_file)?;
|
||||||
|
|
||||||
|
let mut stack_to_tun = tokio::spawn(async move {
|
||||||
|
while let Some(Ok(frame)) = stack_stream.next().await {
|
||||||
|
let mut written = 0;
|
||||||
|
while written < frame.len() {
|
||||||
|
let mut guard = match tun_write_stream.writable().await {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = guard.try_io(|inner| {
|
||||||
|
let res = unsafe { libc::write(inner.as_raw_fd(), frame[written..].as_ptr() as *const libc::c_void, frame.len() - written) };
|
||||||
|
if res < 0 {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
if err.kind() == std::io::ErrorKind::WouldBlock {
|
||||||
|
Err(err)
|
||||||
|
} else {
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(Ok(n)) if n > 0 => written += n as usize,
|
||||||
|
Ok(Ok(_)) => break,
|
||||||
|
Ok(Err(_)) => continue,
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let proxy_addr = config.local_proxy.bind_addr.clone();
|
||||||
|
let mut tcp_accept_task = tokio::spawn(async move {
|
||||||
|
if let Some(mut listener) = tcp_listener {
|
||||||
|
while let Some((mut stream, _local, remote)) = listener.next().await {
|
||||||
|
let proxy_addr = proxy_addr.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if debug { tracing::info!("Native TUN intercepted TCP to {}", remote); }
|
||||||
|
if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await {
|
||||||
|
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; }
|
||||||
|
|
||||||
|
let ip = remote.ip();
|
||||||
|
let port = remote.port();
|
||||||
|
let mut req = vec![5, 1, 0];
|
||||||
|
match ip {
|
||||||
|
std::net::IpAddr::V4(v4) => {
|
||||||
|
req.push(1);
|
||||||
|
req.extend_from_slice(&v4.octets());
|
||||||
|
}
|
||||||
|
std::net::IpAddr::V6(v6) => {
|
||||||
|
req.push(4);
|
||||||
|
req.extend_from_slice(&v6.octets());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.extend_from_slice(&port.to_be_bytes());
|
||||||
|
if socks.write_all(&req).await.is_err() { return; }
|
||||||
|
|
||||||
|
let mut rep = [0u8; 10];
|
||||||
|
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
|
||||||
|
|
||||||
|
let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::info!("NATIVE TUN (Android) tunnel active.");
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown.changed() => {}
|
||||||
|
_ = &mut runner_task => {}
|
||||||
|
_ = &mut tun_to_stack => {}
|
||||||
|
_ = &mut stack_to_tun => {}
|
||||||
|
_ = &mut tcp_accept_task => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Deactivating NATIVE TUN tunnel...");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
pub async fn run_native_tunnel_from_fd(
|
||||||
|
_config: crate::config::ClientConfig,
|
||||||
|
_shutdown: watch::Receiver<bool>,
|
||||||
|
_fd: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
Err(anyhow!("Native TUN from FD is only supported on Android"))
|
||||||
|
}
|
||||||
|
|
@ -355,7 +355,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_stopClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rt) = runtime {
|
if let Some(rt) = runtime {
|
||||||
rt.shutdown_timeout(std::time::Duration::from_secs(3));
|
rt.shutdown_background();
|
||||||
}
|
}
|
||||||
|
|
||||||
add_log("OSTP SDK: Client successfully stopped".to_string());
|
add_log("OSTP SDK: Client successfully stopped".to_string());
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,4 @@ uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
simple-dns = "0.11.3"
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@ use portable_atomic::AtomicU64;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::{header, Request, StatusCode, Uri},
|
http::{header, StatusCode, Uri},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse},
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
|
@ -51,6 +50,7 @@ pub struct ApiState {
|
||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
pub reality_query: String,
|
pub reality_query: String,
|
||||||
pub config_path: Option<std::path::PathBuf>,
|
pub config_path: Option<std::path::PathBuf>,
|
||||||
|
pub dns_server: std::sync::Arc<crate::dns::DnsServer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API configuration ────────────────────────────────────────────────────────
|
// ── API configuration ────────────────────────────────────────────────────────
|
||||||
|
|
@ -209,7 +209,10 @@ pub fn create_api_router(state: ApiState) -> Router {
|
||||||
.route("/users/{key}/limit", put(handle_set_limit))
|
.route("/users/{key}/limit", put(handle_set_limit))
|
||||||
.route("/users/{key}/reset", post(handle_reset_stats))
|
.route("/users/{key}/reset", post(handle_reset_stats))
|
||||||
.route("/subscribe/{key}", get(handle_subscribe))
|
.route("/subscribe/{key}", get(handle_subscribe))
|
||||||
.route("/login", post(handle_login));
|
.route("/login", post(handle_login))
|
||||||
|
.route("/dns/config", get(handle_get_dns_config).post(handle_post_dns_config))
|
||||||
|
.route("/dns/queries", get(handle_get_dns_queries))
|
||||||
|
.route("/dns/blocklists/refresh", post(handle_refresh_blocklists));
|
||||||
|
|
||||||
let webpath = state.webpath.clone();
|
let webpath = state.webpath.clone();
|
||||||
let webpath = webpath.trim_matches('/');
|
let webpath = webpath.trim_matches('/');
|
||||||
|
|
@ -247,6 +250,7 @@ pub async fn start_api_server(
|
||||||
server_port: u16,
|
server_port: u16,
|
||||||
reality_query: String,
|
reality_query: String,
|
||||||
config_path: Option<std::path::PathBuf>,
|
config_path: Option<std::path::PathBuf>,
|
||||||
|
dns_server: std::sync::Arc<crate::dns::DnsServer>,
|
||||||
) {
|
) {
|
||||||
let state = ApiState {
|
let state = ApiState {
|
||||||
access_keys,
|
access_keys,
|
||||||
|
|
@ -260,6 +264,7 @@ pub async fn start_api_server(
|
||||||
server_port,
|
server_port,
|
||||||
reality_query,
|
reality_query,
|
||||||
config_path,
|
config_path,
|
||||||
|
dns_server,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = create_api_router(state);
|
let app = create_api_router(state);
|
||||||
|
|
@ -740,7 +745,16 @@ async fn handle_subscribe(
|
||||||
|
|
||||||
// If client requests plain text, return ostp:// share link
|
// If client requests plain text, return ostp:// share link
|
||||||
if accept.contains("text/plain") {
|
if accept.contains("text/plain") {
|
||||||
let link = format!("ostp://{}@{}:{}{}", key, state.server_host, state.server_port, state.reality_query);
|
let dns_enabled = state.dns_server.config.read().await.enabled;
|
||||||
|
let mut rq = state.reality_query.clone();
|
||||||
|
if dns_enabled {
|
||||||
|
if rq.is_empty() {
|
||||||
|
rq = "?owndns=true".to_string();
|
||||||
|
} else {
|
||||||
|
rq = format!("{}&owndns=true", rq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let link = format!("ostp://{}@{}:{}{}", key, state.server_host, state.server_port, rq);
|
||||||
return (StatusCode::OK, Json(serde_json::json!({
|
return (StatusCode::OK, Json(serde_json::json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"data": link
|
"data": link
|
||||||
|
|
@ -778,45 +792,95 @@ async fn handle_subscribe(
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── DNS API Handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn handle_get_dns_config(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<serde_json::Value>();
|
||||||
|
}
|
||||||
|
let cfg = state.dns_server.config.read().await.clone();
|
||||||
|
(StatusCode::OK, ApiResponse::success(serde_json::to_value(cfg).unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_post_dns_config(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Json(body): Json<crate::dns::DnsConfig>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<bool>();
|
||||||
|
}
|
||||||
|
// Update in-memory config
|
||||||
|
let should_refresh = body.enabled && !body.adblock_urls.is_empty();
|
||||||
|
{
|
||||||
|
let mut cfg = state.dns_server.config.write().await;
|
||||||
|
*cfg = body;
|
||||||
|
}
|
||||||
|
// Reload blocklists if enabled
|
||||||
|
if should_refresh {
|
||||||
|
let dns = state.dns_server.clone();
|
||||||
|
tokio::spawn(async move { dns.update_blocklists().await; });
|
||||||
|
}
|
||||||
|
(StatusCode::OK, ApiResponse::success(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_get_dns_queries(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<Vec<serde_json::Value>>();
|
||||||
|
}
|
||||||
|
let queries = state.dns_server.get_queries().await;
|
||||||
|
let data: Vec<serde_json::Value> = queries.iter().map(|q| serde_json::to_value(q).unwrap_or_default()).collect();
|
||||||
|
(StatusCode::OK, ApiResponse::success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_refresh_blocklists(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !check_token(&state, &headers) {
|
||||||
|
return api_unauthorized::<bool>();
|
||||||
|
}
|
||||||
|
let dns = state.dns_server.clone();
|
||||||
|
tokio::spawn(async move { dns.update_blocklists().await; });
|
||||||
|
(StatusCode::OK, ApiResponse::success(true))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
fn make_test_state(webpath: &str) -> ApiState {
|
||||||
fn test_router_creation() {
|
ApiState {
|
||||||
let state = ApiState {
|
|
||||||
access_keys: Arc::new(RwLock::new(HashMap::new())),
|
access_keys: Arc::new(RwLock::new(HashMap::new())),
|
||||||
user_stats: Arc::new(RwLock::new(HashMap::new())),
|
user_stats: Arc::new(RwLock::new(HashMap::new())),
|
||||||
start_time: std::time::Instant::now(),
|
start_time: std::time::Instant::now(),
|
||||||
session_token: Arc::new(RwLock::new(None)),
|
session_token: Arc::new(RwLock::new(None)),
|
||||||
webpath: "bNAzr8Ss".to_string(),
|
webpath: webpath.to_string(),
|
||||||
username: "admin".to_string(),
|
username: "admin".to_string(),
|
||||||
password_hash: "hash".to_string(),
|
password_hash: "hash".to_string(),
|
||||||
server_host: "127.0.0.1".to_string(),
|
server_host: "127.0.0.1".to_string(),
|
||||||
server_port: 50000,
|
server_port: 50000,
|
||||||
reality_query: "".to_string(),
|
reality_query: "".to_string(),
|
||||||
config_path: None,
|
config_path: None,
|
||||||
};
|
dns_server: crate::dns::DnsServer::new(Default::default()),
|
||||||
// This should not panic
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_router_creation() {
|
||||||
|
let state = make_test_state("bNAzr8Ss");
|
||||||
let _router = create_api_router(state);
|
let _router = create_api_router(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_router_creation_empty_webpath() {
|
fn test_router_creation_empty_webpath() {
|
||||||
let state = ApiState {
|
let state = make_test_state("");
|
||||||
access_keys: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
user_stats: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
start_time: std::time::Instant::now(),
|
|
||||||
session_token: Arc::new(RwLock::new(None)),
|
|
||||||
webpath: "".to_string(),
|
|
||||||
username: "admin".to_string(),
|
|
||||||
password_hash: "hash".to_string(),
|
|
||||||
server_host: "127.0.0.1".to_string(),
|
|
||||||
server_port: 50000,
|
|
||||||
reality_query: "".to_string(),
|
|
||||||
config_path: None,
|
|
||||||
};
|
|
||||||
// This should not panic
|
|
||||||
let _router = create_api_router(state);
|
let _router = create_api_router(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{RwLock, Mutex};
|
||||||
|
use simple_dns::{Packet, rdata::RData, ResourceRecord, CLASS, TYPE, QTYPE};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DnsConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub doh_upstream: String,
|
||||||
|
pub adblock_urls: Vec<String>,
|
||||||
|
pub custom_domains: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DnsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
doh_upstream: "https://cloudflare-dns.com/dns-query".to_string(),
|
||||||
|
adblock_urls: vec![],
|
||||||
|
custom_domains: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DnsQueryLog {
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub domain: String,
|
||||||
|
pub client_ip: String,
|
||||||
|
pub blocked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DnsServer {
|
||||||
|
pub config: RwLock<DnsConfig>,
|
||||||
|
adblock_trie: RwLock<HashSet<String>>, // Simplified to HashSet for now, or maybe a suffix tree
|
||||||
|
query_log: Mutex<VecDeque<DnsQueryLog>>,
|
||||||
|
reqwest_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsServer {
|
||||||
|
pub fn new(config: DnsConfig) -> Arc<Self> {
|
||||||
|
let server = Arc::new(Self {
|
||||||
|
config: RwLock::new(config.clone()),
|
||||||
|
adblock_trie: RwLock::new(HashSet::new()),
|
||||||
|
query_log: Mutex::new(VecDeque::with_capacity(1000)),
|
||||||
|
reqwest_client: reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn a background task to download blocklists
|
||||||
|
if config.enabled && !config.adblock_urls.is_empty() {
|
||||||
|
let server_clone = server.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
server_clone.update_blocklists().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_blocklists(&self) {
|
||||||
|
let urls = {
|
||||||
|
let cfg = self.config.read().await;
|
||||||
|
cfg.adblock_urls.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_blocked = HashSet::new();
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
if let Ok(resp) = self.reqwest_client.get(&url).send().await {
|
||||||
|
if let Ok(text) = resp.text().await {
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Support standard hosts format: "0.0.0.0 ads.google.com" or just "ads.google.com"
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
let domain = if parts.len() >= 2 && (parts[0] == "0.0.0.0" || parts[0] == "127.0.0.1") {
|
||||||
|
parts[1]
|
||||||
|
} else {
|
||||||
|
parts[0]
|
||||||
|
};
|
||||||
|
new_blocked.insert(domain.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Loaded {} domains into AdBlock engine", new_blocked.len());
|
||||||
|
*self.adblock_trie.write().await = new_blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve(&self, payload: &[u8], client_ip: std::net::IpAddr) -> Option<Vec<u8>> {
|
||||||
|
let cfg = self.config.read().await;
|
||||||
|
if !cfg.enabled {
|
||||||
|
return None; // If DNS is disabled, fallback to standard UDP proxying
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse DNS packet
|
||||||
|
let packet = match Packet::parse(payload) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if packet.questions.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let question = &packet.questions[0];
|
||||||
|
let qname = question.qname.to_string().to_lowercase();
|
||||||
|
|
||||||
|
// Check Custom Domains
|
||||||
|
if let Some(ip_str) = cfg.custom_domains.get(&qname) {
|
||||||
|
if let Ok(ip) = ip_str.parse::<std::net::Ipv4Addr>() {
|
||||||
|
if question.qtype == QTYPE::TYPE(TYPE::A) {
|
||||||
|
let mut response = Packet::new_reply(packet.id());
|
||||||
|
response.questions.push(question.clone());
|
||||||
|
response.answers.push(ResourceRecord::new(
|
||||||
|
question.qname.clone(),
|
||||||
|
CLASS::IN,
|
||||||
|
60,
|
||||||
|
RData::A(ip.into()),
|
||||||
|
));
|
||||||
|
self.log_query(qname, client_ip.to_string(), false).await;
|
||||||
|
return response.build_bytes_vec().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check AdBlock (Suffix matching not implemented in this simple hashset, for full pi-hole we need suffix match)
|
||||||
|
// Let's do a simple suffix check
|
||||||
|
let blocked = {
|
||||||
|
let blocked_domains = self.adblock_trie.read().await;
|
||||||
|
let mut parts: Vec<&str> = qname.split('.').collect();
|
||||||
|
let mut is_blocked = false;
|
||||||
|
while !parts.is_empty() {
|
||||||
|
let suffix = parts.join(".");
|
||||||
|
if blocked_domains.contains(&suffix) {
|
||||||
|
is_blocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parts.remove(0);
|
||||||
|
}
|
||||||
|
is_blocked
|
||||||
|
};
|
||||||
|
|
||||||
|
if blocked {
|
||||||
|
let mut response = Packet::new_reply(packet.id());
|
||||||
|
response.questions.push(question.clone());
|
||||||
|
self.log_query(qname, client_ip.to_string(), true).await;
|
||||||
|
return response.build_bytes_vec().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to DoH
|
||||||
|
let doh_url = cfg.doh_upstream.clone();
|
||||||
|
drop(cfg); // Release config lock before making network request
|
||||||
|
|
||||||
|
if let Ok(resp) = self.reqwest_client.post(&doh_url)
|
||||||
|
.header("Content-Type", "application/dns-message")
|
||||||
|
.header("Accept", "application/dns-message")
|
||||||
|
.body(payload.to_vec())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if resp.status().is_success() {
|
||||||
|
if let Ok(bytes) = resp.bytes().await {
|
||||||
|
self.log_query(qname, client_ip.to_string(), false).await;
|
||||||
|
return Some(bytes.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn log_query(&self, domain: String, client_ip: String, blocked: bool) {
|
||||||
|
let mut log = self.query_log.lock().await;
|
||||||
|
if log.len() >= 1000 {
|
||||||
|
log.pop_front();
|
||||||
|
}
|
||||||
|
log.push_back(DnsQueryLog {
|
||||||
|
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
||||||
|
domain,
|
||||||
|
client_ip,
|
||||||
|
blocked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_queries(&self) -> Vec<DnsQueryLog> {
|
||||||
|
let log = self.query_log.lock().await;
|
||||||
|
log.iter().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ pub mod transport;
|
||||||
pub mod relay_node;
|
pub mod relay_node;
|
||||||
mod relay;
|
mod relay;
|
||||||
mod signal;
|
mod signal;
|
||||||
|
pub mod dns;
|
||||||
|
|
||||||
pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
|
pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
|
||||||
pub use api::ApiConfig;
|
pub use api::ApiConfig;
|
||||||
|
|
@ -59,6 +60,8 @@ pub(crate) enum UiEvent {
|
||||||
pub(crate) struct RemoteState {
|
pub(crate) struct RemoteState {
|
||||||
pub data_tx: mpsc::UnboundedSender<Bytes>,
|
pub data_tx: mpsc::UnboundedSender<Bytes>,
|
||||||
pub cancel_tx: mpsc::Sender<()>,
|
pub cancel_tx: mpsc::Sender<()>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub is_dns: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ───────────────────────────────────────────────────────────────
|
// ── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -73,6 +76,7 @@ pub async fn run_server(
|
||||||
debug: bool,
|
debug: bool,
|
||||||
reality_query: Option<String>,
|
reality_query: Option<String>,
|
||||||
reality_config: Option<RealityServerConfig>,
|
reality_config: Option<RealityServerConfig>,
|
||||||
|
dns_config: Option<dns::DnsConfig>,
|
||||||
config_path: Option<std::path::PathBuf>,
|
config_path: Option<std::path::PathBuf>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut keys_map = HashMap::new();
|
let mut keys_map = HashMap::new();
|
||||||
|
|
@ -240,6 +244,9 @@ pub async fn run_server(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize DNS server
|
||||||
|
let dns_server = dns::DnsServer::new(dns_config.unwrap_or_default());
|
||||||
|
|
||||||
// Spawn Management API if configured
|
// Spawn Management API if configured
|
||||||
if let Some(api_cfg) = api_config {
|
if let Some(api_cfg) = api_config {
|
||||||
if api_cfg.enabled {
|
if api_cfg.enabled {
|
||||||
|
|
@ -252,8 +259,9 @@ pub async fn run_server(
|
||||||
let server_host = server_public_ip.unwrap_or_else(|| parts.get(1).unwrap_or(&"0.0.0.0").to_string());
|
let server_host = server_public_ip.unwrap_or_else(|| parts.get(1).unwrap_or(&"0.0.0.0").to_string());
|
||||||
let rq = reality_query.clone().unwrap_or_default();
|
let rq = reality_query.clone().unwrap_or_default();
|
||||||
let config_path_api = config_path.clone();
|
let config_path_api = config_path.clone();
|
||||||
|
let dns_server_api = dns_server.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq, config_path_api).await;
|
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq, config_path_api, dns_server_api).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +330,7 @@ pub async fn run_server(
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, tls_config) => {
|
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, tls_config, dns_server) => {
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
tracing::error!("Server error: {e}");
|
tracing::error!("Server error: {e}");
|
||||||
}
|
}
|
||||||
|
|
@ -348,6 +356,7 @@ async fn run_server_loop(
|
||||||
outbound: Option<OutboundConfig>,
|
outbound: Option<OutboundConfig>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
||||||
|
dns_server: std::sync::Arc<dns::DnsServer>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
|
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
|
||||||
let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>();
|
let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>();
|
||||||
|
|
@ -545,6 +554,7 @@ async fn run_server_loop(
|
||||||
stream_tx.clone(),
|
stream_tx.clone(),
|
||||||
connect_tx.clone(),
|
connect_tx.clone(),
|
||||||
outbound.clone(),
|
outbound.clone(),
|
||||||
|
dns_server.clone(),
|
||||||
debug,
|
debug,
|
||||||
).await?;
|
).await?;
|
||||||
}
|
}
|
||||||
|
|
@ -577,7 +587,7 @@ async fn run_server_loop(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
remotes.insert((session_id, stream_id), RemoteState { data_tx, cancel_tx });
|
remotes.insert((session_id, stream_id), RemoteState { data_tx, cancel_tx, is_dns: false });
|
||||||
let _ = relay::send_relay_to_stream(session_id, stream_id, RelayMessage::ConnectOk, &mut dispatcher, &socket, &ui_event_tx).await;
|
let _ = relay::send_relay_to_stream(session_id, stream_id, RelayMessage::ConnectOk, &mut dispatcher, &socket, &ui_event_tx).await;
|
||||||
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay CONNECT ok for [{session_id}:{stream_id}] -> {target}")));
|
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay CONNECT ok for [{session_id}:{stream_id}] -> {target}")));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ use tokio::net::UdpSocket;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::dispatcher::Dispatcher;
|
use crate::dispatcher::Dispatcher;
|
||||||
|
use crate::dns::DnsServer;
|
||||||
use crate::outbound::{self, OutboundConfig};
|
use crate::outbound::{self, OutboundConfig};
|
||||||
use crate::{RemoteState, UiEvent};
|
use crate::{RemoteState, UiEvent};
|
||||||
|
|
||||||
pub async fn handle_relay_message(
|
pub async fn handle_relay_message(
|
||||||
_peer_addr: std::net::SocketAddr,
|
peer_addr: std::net::SocketAddr,
|
||||||
session_id: u32,
|
session_id: u32,
|
||||||
stream_id: u16,
|
stream_id: u16,
|
||||||
payload: Bytes,
|
payload: Bytes,
|
||||||
|
|
@ -23,11 +24,53 @@ pub async fn handle_relay_message(
|
||||||
stream_tx: mpsc::UnboundedSender<(u32, u16, Vec<u8>)>,
|
stream_tx: mpsc::UnboundedSender<(u32, u16, Vec<u8>)>,
|
||||||
connect_tx: mpsc::UnboundedSender<(u32, u16, String, Result<(tokio::net::tcp::OwnedWriteHalf, mpsc::Sender<()>), String>)>,
|
connect_tx: mpsc::UnboundedSender<(u32, u16, String, Result<(tokio::net::tcp::OwnedWriteHalf, mpsc::Sender<()>), String>)>,
|
||||||
outbound_cfg: Option<OutboundConfig>,
|
outbound_cfg: Option<OutboundConfig>,
|
||||||
|
dns_server: std::sync::Arc<DnsServer>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match RelayMessage::decode(&payload)? {
|
match RelayMessage::decode(&payload)? {
|
||||||
RelayMessage::Connect(target) => {
|
RelayMessage::Connect(target) => {
|
||||||
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay CONNECT start for [{session_id}:{stream_id}] -> {target}")));
|
let _ = ui_event_tx.send(UiEvent::Log(format!("Relay CONNECT start for [{session_id}:{stream_id}] -> {target}")));
|
||||||
|
|
||||||
|
// ── DNS Interception ────────────────────────────────────────────────
|
||||||
|
// If client is connecting to port 53 (DNS), we handle it locally
|
||||||
|
// instead of opening a real UDP/TCP socket to the destination.
|
||||||
|
//
|
||||||
|
// Protocol flow:
|
||||||
|
// 1. Client sends Connect("8.8.8.8:53") → we reply ConnectOk
|
||||||
|
// 2. Client sends Data(<dns_query_bytes>) → we resolve & reply Data(<dns_response>) + Close
|
||||||
|
if is_dns_target(&target) {
|
||||||
|
let client_ip = peer_addr.ip();
|
||||||
|
let dns_srv = dns_server.clone();
|
||||||
|
let stream_tx_dns = stream_tx.clone();
|
||||||
|
let (cancel_tx, _) = mpsc::channel::<()>(1);
|
||||||
|
|
||||||
|
// Channel: relay.rs Data handler → DNS resolution task
|
||||||
|
let (dns_query_tx, mut dns_query_rx) = mpsc::unbounded_channel::<Bytes>();
|
||||||
|
|
||||||
|
// Spawn task that waits for the DNS query payload and resolves it
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Some(query_bytes) = dns_query_rx.recv().await {
|
||||||
|
if let Some(resp_bytes) = dns_srv.resolve(&query_bytes, client_ip).await {
|
||||||
|
let _ = stream_tx_dns.send((session_id, stream_id, resp_bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always close the stream after responding
|
||||||
|
let _ = stream_tx_dns.send((session_id, stream_id, Vec::new()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store as a RemoteState — Data messages will be forwarded to dns_query_tx
|
||||||
|
remotes.insert((session_id, stream_id), RemoteState {
|
||||||
|
data_tx: dns_query_tx,
|
||||||
|
cancel_tx,
|
||||||
|
is_dns: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tell the client we are ready to receive its DNS query
|
||||||
|
send_relay_to_stream(session_id, stream_id, RelayMessage::ConnectOk, dispatcher, socket, ui_event_tx).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Normal TCP Connect ──────────────────────────────────────────────
|
||||||
let target_clone = target.clone();
|
let target_clone = target.clone();
|
||||||
let connect_tx_clone = connect_tx.clone();
|
let connect_tx_clone = connect_tx.clone();
|
||||||
let stream_tx_clone = stream_tx.clone();
|
let stream_tx_clone = stream_tx.clone();
|
||||||
|
|
@ -93,6 +136,11 @@ pub async fn handle_relay_message(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the target address is a DNS server (port 53)
|
||||||
|
fn is_dns_target(target: &str) -> bool {
|
||||||
|
target.ends_with(":53")
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_relay_to_stream(
|
pub async fn send_relay_to_stream(
|
||||||
session_id: u32,
|
session_id: u32,
|
||||||
stream_id: u16,
|
stream_id: u16,
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ struct ServerConfig {
|
||||||
api: Option<ApiConfig>,
|
api: Option<ApiConfig>,
|
||||||
fallback: Option<FallbackCfg>,
|
fallback: Option<FallbackCfg>,
|
||||||
transport: Option<TransportConfigRaw>,
|
transport: Option<TransportConfigRaw>,
|
||||||
|
dns: Option<ostp_server::dns::DnsConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Конфигурация Relay-узла в config.json
|
/// Конфигурация Relay-узла в config.json
|
||||||
|
|
@ -895,8 +896,10 @@ async fn run_app() -> Result<()> {
|
||||||
})
|
})
|
||||||
}).collect::<Vec<_>>();
|
}).collect::<Vec<_>>();
|
||||||
let host = get_or_ask_public_ip(&args.config);
|
let host = get_or_ask_public_ip(&args.config);
|
||||||
|
// Build DNS config and set owndns flag in subscribe links if DNS enabled
|
||||||
|
let dns_cfg = server_cfg.dns;
|
||||||
// Pass all listen addresses for multi-listener support
|
// Pass all listen addresses for multi-listener support
|
||||||
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, rq, rc, Some(args.config)).await?;
|
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, rq, rc, dns_cfg, Some(args.config)).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
run_client_directly(client_cfg).await?;
|
run_client_directly(client_cfg).await?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue