mirror of https://github.com/ospab/ostp.git
feat: integrate DNS Transport (DNS Proxy) as last resort transport
- Implement DnsTransportClient and polling logic - Implement DnsTransportServer for TXT/NULL record handling - Add dns_prober to find best public resolvers by region - Update React GUI (Desktop) to support DNS Proxy and i18n - Update Flutter App to support DNS Proxy settings - Update CLI Setup Wizard to generate new v0.3.1 config with dns_transport block - Add Wiki documentation for DNS Transport
This commit is contained in:
parent
a955946fdb
commit
3f1adbc58f
|
|
@ -1491,6 +1491,7 @@ name = "ostp-core"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0c5c52a57d899c05428c116898941761a2ed83c2
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -93,7 +93,15 @@ pub enum OutboundConfig {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TransportConfig {
|
pub struct TransportConfig {
|
||||||
#[serde(default = "default_transport_mode")]
|
#[serde(default = "default_transport_mode")]
|
||||||
pub r#type: String, // "udp" or "uot"
|
pub r#type: String, // "udp", "uot", or "dns"
|
||||||
|
|
||||||
|
// Settings for DNS transport
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domain: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub resolver: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pubkey: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_transport_mode() -> String { "udp".to_string() }
|
fn default_transport_mode() -> String { "udp".to_string() }
|
||||||
|
|
@ -102,6 +110,9 @@ impl Default for TransportConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
r#type: default_transport_mode(),
|
r#type: default_transport_mode(),
|
||||||
|
domain: None,
|
||||||
|
resolver: None,
|
||||||
|
pubkey: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use ostp_core::dns::{
|
||||||
|
DnsPacket, DnsRecordType, encode_payload_to_domain, decode_domain_to_payload,
|
||||||
|
};
|
||||||
|
use crate::transport::Transport;
|
||||||
|
|
||||||
|
pub async fn start_dns_transport(domain: String, resolver: String, _pubkey: Option<String>) -> std::io::Result<Transport> {
|
||||||
|
let (app_tx, transport_rx) = mpsc::channel::<Bytes>(100);
|
||||||
|
let (transport_tx, app_rx) = mpsc::channel::<Bytes>(100);
|
||||||
|
|
||||||
|
let resolver_addr = if resolver.contains(':') {
|
||||||
|
resolver.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}:53", resolver)
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
socket.connect(&resolver_addr).await?;
|
||||||
|
let socket = Arc::new(socket);
|
||||||
|
|
||||||
|
let sock_rx = socket.clone();
|
||||||
|
let sock_tx = socket;
|
||||||
|
let base_domain = domain.clone();
|
||||||
|
|
||||||
|
// Send task (reads from app, encodes into DNS TXT, sends to UDP socket)
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut rx = transport_rx;
|
||||||
|
loop {
|
||||||
|
let data_opt = tokio::select! {
|
||||||
|
res = rx.recv() => res,
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(2)) => Some(Bytes::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = match data_opt {
|
||||||
|
Some(d) => d,
|
||||||
|
None => break, // App closed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encode data to base32 domain
|
||||||
|
let fqdn = encode_payload_to_domain(&data, &base_domain);
|
||||||
|
let id: u16 = rand::thread_rng().gen();
|
||||||
|
|
||||||
|
// Randomly choose TXT or NULL for diversity (as requested)
|
||||||
|
let qtype = if rand::thread_rng().gen_bool(0.5) {
|
||||||
|
DnsRecordType::TXT
|
||||||
|
} else {
|
||||||
|
DnsRecordType::NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet = DnsPacket::new_query(id, &fqdn, qtype);
|
||||||
|
let encoded = packet.encode();
|
||||||
|
|
||||||
|
if let Err(e) = sock_tx.send(&encoded).await {
|
||||||
|
tracing::warn!("DNS transport send error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive task (reads from UDP socket, decodes DNS answer, sends to app)
|
||||||
|
let base_domain_rx = domain.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; 65535];
|
||||||
|
loop {
|
||||||
|
match sock_rx.recv(&mut buf).await {
|
||||||
|
Ok(n) => {
|
||||||
|
if let Some(packet) = DnsPacket::decode(&buf[..n]) {
|
||||||
|
for answer in packet.answers {
|
||||||
|
if answer.rtype == DnsRecordType::TXT || answer.rtype == DnsRecordType::NULL {
|
||||||
|
// If it's a TXT record, the response might be base32 encoded payload?
|
||||||
|
// Actually, dnstt puts the payload in the TXT/NULL record data.
|
||||||
|
// We'll just assume the rdata is the raw payload, or base32 encoded if it was sent as such.
|
||||||
|
// Let's just pass the raw data (TXT strings are decoded in DnsPacket::decode)
|
||||||
|
|
||||||
|
// Wait, dnstt server responds with raw bytes in NULL, and base32/chunked strings in TXT.
|
||||||
|
// Our `DnsPacket::decode` already handles extracting TXT string bytes or NULL raw bytes into `rdata`.
|
||||||
|
// Let's just send `rdata` to the app.
|
||||||
|
if transport_tx.send(Bytes::from(answer.rdata)).await.is_err() {
|
||||||
|
return; // App closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("DNS transport recv error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Transport::Dns {
|
||||||
|
tx: app_tx,
|
||||||
|
rx: Arc::new(Mutex::new(app_rx)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
pub mod dns;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::UdpSocket;
|
use tokio::net::UdpSocket;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
@ -9,6 +9,10 @@ pub enum Transport {
|
||||||
Uot {
|
Uot {
|
||||||
tx: tokio::sync::mpsc::Sender<Bytes>,
|
tx: tokio::sync::mpsc::Sender<Bytes>,
|
||||||
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
|
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
|
||||||
|
},
|
||||||
|
Dns {
|
||||||
|
tx: tokio::sync::mpsc::Sender<Bytes>,
|
||||||
|
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,8 +20,8 @@ impl Transport {
|
||||||
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
|
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Self::Udp(sock) => sock.send(frame).await,
|
Self::Udp(sock) => sock.send(frame).await,
|
||||||
Self::Uot { tx, .. } => {
|
Self::Uot { tx, .. } | Self::Dns { tx, .. } => {
|
||||||
tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed"))?;
|
tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?;
|
||||||
Ok(frame.len())
|
Ok(frame.len())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,14 +30,14 @@ impl Transport {
|
||||||
pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> {
|
pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Self::Udp(sock) => sock.send_to(frame, target).await,
|
Self::Udp(sock) => sock.send_to(frame, target).await,
|
||||||
Self::Uot { .. } => self.send(frame).await,
|
Self::Uot { .. } | Self::Dns { .. } => self.send(frame).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
|
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Self::Udp(sock) => sock.recv(buf).await,
|
Self::Udp(sock) => sock.recv(buf).await,
|
||||||
Self::Uot { rx, .. } => {
|
Self::Uot { rx, .. } | Self::Dns { rx, .. } => {
|
||||||
let mut rx = rx.lock().await;
|
let mut rx = rx.lock().await;
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Some(bytes) => {
|
Some(bytes) => {
|
||||||
|
|
@ -41,7 +45,7 @@ impl Transport {
|
||||||
buf[..len].copy_from_slice(&bytes[..len]);
|
buf[..len].copy_from_slice(&bytes[..len]);
|
||||||
Ok(len)
|
Ok(len)
|
||||||
}
|
}
|
||||||
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed")),
|
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +54,7 @@ impl Transport {
|
||||||
pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
|
pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
|
||||||
match self {
|
match self {
|
||||||
Self::Udp(sock) => sock.local_addr(),
|
Self::Udp(sock) => sock.local_addr(),
|
||||||
Self::Uot { .. } => Ok("0.0.0.0:0".parse().unwrap()),
|
Self::Uot { .. } | Self::Dns { .. } => Ok("0.0.0.0:0".parse().unwrap()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub async fn dial_tcp(
|
||||||
server: &str,
|
server: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
access_key: &str,
|
access_key: &str,
|
||||||
_transport: &TransportConfig,
|
transport_cfg: &TransportConfig,
|
||||||
_multiplex: &MultiplexConfig,
|
_multiplex: &MultiplexConfig,
|
||||||
) -> Result<TcpStream> {
|
) -> Result<TcpStream> {
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||||
|
|
@ -18,8 +18,19 @@ pub async fn dial_tcp(
|
||||||
let client_stream = tokio::net::TcpStream::connect(local_addr).await?;
|
let client_stream = tokio::net::TcpStream::connect(local_addr).await?;
|
||||||
let (mut server_stream, _) = listener.accept().await?;
|
let (mut server_stream, _) = listener.accept().await?;
|
||||||
|
|
||||||
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
let transport = match transport_cfg.r#type.as_str() {
|
||||||
udp.connect((server, port)).await?;
|
"dns" => {
|
||||||
|
let domain = transport_cfg.domain.clone().unwrap_or_else(|| "tunnel.example.com".to_string());
|
||||||
|
let resolver = transport_cfg.resolver.clone().unwrap_or_else(|| "8.8.8.8".to_string());
|
||||||
|
crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await?
|
||||||
|
}
|
||||||
|
// Fallback to UDP for now if unknown
|
||||||
|
_ => {
|
||||||
|
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
udp.connect((server, port)).await?;
|
||||||
|
crate::transport::Transport::Udp(std::sync::Arc::new(udp))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut psk = [0u8; 32];
|
let mut psk = [0u8; 32];
|
||||||
let key_bytes = access_key.as_bytes();
|
let key_bytes = access_key.as_bytes();
|
||||||
|
|
@ -50,7 +61,7 @@ pub async fn dial_tcp(
|
||||||
// Spawn bridge task
|
// Spawn bridge task
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Start) {
|
if let Ok(action) = machine.on_event(OstpEvent::Start) {
|
||||||
handle_action(action, &udp, &mut server_stream).await;
|
handle_action(action, &transport, &mut server_stream).await;
|
||||||
}
|
}
|
||||||
let mut buf = [0u8; 65535];
|
let mut buf = [0u8; 65535];
|
||||||
let mut udp_buf = [0u8; 65535];
|
let mut udp_buf = [0u8; 65535];
|
||||||
|
|
@ -60,17 +71,17 @@ pub async fn dial_tcp(
|
||||||
Ok(n) = server_stream.read(&mut buf) => {
|
Ok(n) = server_stream.read(&mut buf) => {
|
||||||
if n == 0 { break; }
|
if n == 0 { break; }
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) {
|
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) {
|
||||||
handle_action(action, &udp, &mut server_stream).await;
|
handle_action(action, &transport, &mut server_stream).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(n) = udp.recv(&mut udp_buf) => {
|
Ok(n) = transport.recv(&mut udp_buf) => {
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&udp_buf[..n]))) {
|
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&udp_buf[..n]))) {
|
||||||
handle_action(action, &udp, &mut server_stream).await;
|
handle_action(action, &transport, &mut server_stream).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
|
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
|
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
|
||||||
handle_action(action, &udp, &mut server_stream).await;
|
handle_action(action, &transport, &mut server_stream).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,11 +98,21 @@ pub async fn handle_udp(
|
||||||
server: &str,
|
server: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
access_key: &str,
|
access_key: &str,
|
||||||
_transport: &TransportConfig,
|
transport_cfg: &TransportConfig,
|
||||||
_multiplex: &MultiplexConfig,
|
_multiplex: &MultiplexConfig,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
let transport = match transport_cfg.r#type.as_str() {
|
||||||
udp.connect((server, port)).await?;
|
"dns" => {
|
||||||
|
let domain = transport_cfg.domain.clone().unwrap_or_else(|| "tunnel.example.com".to_string());
|
||||||
|
let resolver = transport_cfg.resolver.clone().unwrap_or_else(|| "8.8.8.8".to_string());
|
||||||
|
crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
udp.connect((server, port)).await?;
|
||||||
|
crate::transport::Transport::Udp(std::sync::Arc::new(udp))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut psk = [0u8; 32];
|
let mut psk = [0u8; 32];
|
||||||
let key_bytes = access_key.as_bytes();
|
let key_bytes = access_key.as_bytes();
|
||||||
|
|
@ -126,21 +147,21 @@ pub async fn handle_udp(
|
||||||
|
|
||||||
// Send initial packet with UDP payload
|
// Send initial packet with UDP payload
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Start) {
|
if let Ok(action) = machine.on_event(OstpEvent::Start) {
|
||||||
handle_udp_action(action, &udp).await;
|
handle_udp_action(action, &transport).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the actual UDP payload
|
// Send the actual UDP payload
|
||||||
let relay_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_dst.ip(), target_dst.port()));
|
let relay_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_dst.ip(), target_dst.port()));
|
||||||
let encoded = relay_msg.encode();
|
let encoded = relay_msg.encode();
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
|
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
|
||||||
handle_udp_action(action, &udp).await;
|
handle_udp_action(action, &transport).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send data packet
|
// Send data packet
|
||||||
let data_msg = ostp_core::relay::RelayMessage::Data(payload.to_vec());
|
let data_msg = ostp_core::relay::RelayMessage::Data(payload.to_vec());
|
||||||
let encoded = data_msg.encode();
|
let encoded = data_msg.encode();
|
||||||
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
|
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
|
||||||
handle_udp_action(action, &udp).await;
|
handle_udp_action(action, &transport).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep-alive for a short time to receive response
|
// Keep-alive for a short time to receive response
|
||||||
|
|
@ -148,7 +169,7 @@ pub async fn handle_udp(
|
||||||
let mut buf = [0u8; 8192];
|
let mut buf = [0u8; 8192];
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(100),
|
std::time::Duration::from_millis(100),
|
||||||
udp.recv(&mut buf)
|
transport.recv(&mut buf)
|
||||||
).await {
|
).await {
|
||||||
Ok(Ok(n)) => {
|
Ok(Ok(n)) => {
|
||||||
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
|
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
|
||||||
|
|
@ -160,15 +181,15 @@ pub async fn handle_udp(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_udp_action(action: ProtocolAction, udp: &UdpSocket) {
|
async fn handle_udp_action(action: ProtocolAction, transport: &crate::transport::Transport) {
|
||||||
match action {
|
match action {
|
||||||
ProtocolAction::SendDatagram(data) => {
|
ProtocolAction::SendDatagram(data) => {
|
||||||
let _ = udp.send(&data).await;
|
let _ = transport.send(&data).await;
|
||||||
}
|
}
|
||||||
ProtocolAction::Multiple(actions) => {
|
ProtocolAction::Multiple(actions) => {
|
||||||
for a in actions {
|
for a in actions {
|
||||||
if let ProtocolAction::SendDatagram(data) = a {
|
if let ProtocolAction::SendDatagram(data) = a {
|
||||||
let _ = udp.send(&data).await;
|
let _ = transport.send(&data).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,10 +197,10 @@ async fn handle_udp_action(action: ProtocolAction, udp: &UdpSocket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_action(action: ProtocolAction, udp: &UdpSocket, server_stream: &mut tokio::net::TcpStream) {
|
async fn handle_action(action: ProtocolAction, transport: &crate::transport::Transport, server_stream: &mut tokio::net::TcpStream) {
|
||||||
match action {
|
match action {
|
||||||
ProtocolAction::SendDatagram(data) => {
|
ProtocolAction::SendDatagram(data) => {
|
||||||
let _ = udp.send(&data).await;
|
let _ = transport.send(&data).await;
|
||||||
}
|
}
|
||||||
ProtocolAction::DeliverApp(_stream_id, payload) => {
|
ProtocolAction::DeliverApp(_stream_id, payload) => {
|
||||||
let _ = server_stream.write_all(&payload).await;
|
let _ = server_stream.write_all(&payload).await;
|
||||||
|
|
@ -187,7 +208,7 @@ async fn handle_action(action: ProtocolAction, udp: &UdpSocket, server_stream: &
|
||||||
ProtocolAction::Multiple(actions) => {
|
ProtocolAction::Multiple(actions) => {
|
||||||
for a in actions {
|
for a in actions {
|
||||||
match a {
|
match a {
|
||||||
ProtocolAction::SendDatagram(data) => { let _ = udp.send(&data).await; }
|
ProtocolAction::SendDatagram(data) => { let _ = transport.send(&data).await; }
|
||||||
ProtocolAction::DeliverApp(_stream_id, payload) => { let _ = server_stream.write_all(&payload).await; }
|
ProtocolAction::DeliverApp(_stream_id, payload) => { let _ = server_stream.write_all(&payload).await; }
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ rand.workspace = true
|
||||||
snow.workspace = true
|
snow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
byteorder = "1.5"
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
hmac.workspace = true
|
hmac.workspace = true
|
||||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,413 @@
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
|
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
|
||||||
|
|
||||||
|
/// Encodes a byte slice into Base32 (RFC 4648) without padding, lowercase.
|
||||||
|
pub fn base32_encode(data: &[u8]) -> String {
|
||||||
|
let mut result = String::with_capacity((data.len() * 8 + 4) / 5);
|
||||||
|
let mut buffer = 0u32;
|
||||||
|
let mut bits_left = 0;
|
||||||
|
|
||||||
|
for &b in data {
|
||||||
|
buffer = (buffer << 8) | (b as u32);
|
||||||
|
bits_left += 8;
|
||||||
|
while bits_left >= 5 {
|
||||||
|
bits_left -= 5;
|
||||||
|
let index = ((buffer >> bits_left) & 0x1F) as usize;
|
||||||
|
result.push(BASE32_ALPHABET[index] as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bits_left > 0 {
|
||||||
|
let index = ((buffer << (5 - bits_left)) & 0x1F) as usize;
|
||||||
|
result.push(BASE32_ALPHABET[index] as char);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a Base32 string (case-insensitive, no padding) into a byte vector.
|
||||||
|
pub fn base32_decode(encoded: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut result = Vec::with_capacity(encoded.len() * 5 / 8);
|
||||||
|
let mut buffer = 0u32;
|
||||||
|
let mut bits_left = 0;
|
||||||
|
|
||||||
|
for c in encoded.bytes() {
|
||||||
|
let val = match c {
|
||||||
|
b'a'..=b'z' => c - b'a',
|
||||||
|
b'A'..=b'Z' => c - b'A',
|
||||||
|
b'2'..=b'7' => c - b'2' + 26,
|
||||||
|
_ => return None, // Invalid character
|
||||||
|
};
|
||||||
|
|
||||||
|
buffer = (buffer << 5) | (val as u32);
|
||||||
|
bits_left += 5;
|
||||||
|
|
||||||
|
if bits_left >= 8 {
|
||||||
|
bits_left -= 8;
|
||||||
|
result.push((buffer >> bits_left) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum DnsRecordType {
|
||||||
|
A,
|
||||||
|
CNAME,
|
||||||
|
NULL,
|
||||||
|
TXT,
|
||||||
|
AAAA,
|
||||||
|
Unknown(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for DnsRecordType {
|
||||||
|
fn from(val: u16) -> Self {
|
||||||
|
match val {
|
||||||
|
1 => DnsRecordType::A,
|
||||||
|
5 => DnsRecordType::CNAME,
|
||||||
|
10 => DnsRecordType::NULL,
|
||||||
|
16 => DnsRecordType::TXT,
|
||||||
|
28 => DnsRecordType::AAAA,
|
||||||
|
_ => DnsRecordType::Unknown(val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsRecordType {
|
||||||
|
pub fn as_u16(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
DnsRecordType::A => 1,
|
||||||
|
DnsRecordType::CNAME => 5,
|
||||||
|
DnsRecordType::NULL => 10,
|
||||||
|
DnsRecordType::TXT => 16,
|
||||||
|
DnsRecordType::AAAA => 28,
|
||||||
|
DnsRecordType::Unknown(v) => *v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DnsQuestion {
|
||||||
|
pub name: String,
|
||||||
|
pub qtype: DnsRecordType,
|
||||||
|
pub qclass: u16, // Usually 1 (IN)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DnsAnswer {
|
||||||
|
pub name: String,
|
||||||
|
pub rtype: DnsRecordType,
|
||||||
|
pub rclass: u16,
|
||||||
|
pub ttl: u32,
|
||||||
|
pub rdata: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DnsPacket {
|
||||||
|
pub id: u16,
|
||||||
|
pub flags: u16,
|
||||||
|
pub questions: Vec<DnsQuestion>,
|
||||||
|
pub answers: Vec<DnsAnswer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsPacket {
|
||||||
|
pub fn new_query(id: u16, name: &str, qtype: DnsRecordType) -> Self {
|
||||||
|
DnsPacket {
|
||||||
|
id,
|
||||||
|
flags: 0x0100, // Standard query, recursion desired
|
||||||
|
questions: vec![DnsQuestion {
|
||||||
|
name: name.to_string(),
|
||||||
|
qtype,
|
||||||
|
qclass: 1, // IN
|
||||||
|
}],
|
||||||
|
answers: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_response(id: u16, name: &str, rtype: DnsRecordType, rdata: Vec<u8>) -> Self {
|
||||||
|
DnsPacket {
|
||||||
|
id,
|
||||||
|
flags: 0x8180, // Response, standard query, recursion desired, recursion available
|
||||||
|
questions: vec![DnsQuestion {
|
||||||
|
name: name.to_string(),
|
||||||
|
qtype: rtype.clone(),
|
||||||
|
qclass: 1, // IN
|
||||||
|
}],
|
||||||
|
answers: vec![DnsAnswer {
|
||||||
|
name: name.to_string(),
|
||||||
|
rtype,
|
||||||
|
rclass: 1,
|
||||||
|
ttl: 0, // No caching
|
||||||
|
rdata,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let _ = buf.write_u16::<BigEndian>(self.id);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(self.flags);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(self.questions.len() as u16);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(self.answers.len() as u16);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(0); // Authority PR
|
||||||
|
let _ = buf.write_u16::<BigEndian>(0); // Additional PR
|
||||||
|
|
||||||
|
for q in &self.questions {
|
||||||
|
encode_domain_name(&mut buf, &q.name);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(q.qtype.as_u16());
|
||||||
|
let _ = buf.write_u16::<BigEndian>(q.qclass);
|
||||||
|
}
|
||||||
|
|
||||||
|
for a in &self.answers {
|
||||||
|
encode_domain_name(&mut buf, &a.name);
|
||||||
|
let _ = buf.write_u16::<BigEndian>(a.rtype.as_u16());
|
||||||
|
let _ = buf.write_u16::<BigEndian>(a.rclass);
|
||||||
|
let _ = buf.write_u32::<BigEndian>(a.ttl);
|
||||||
|
|
||||||
|
if a.rtype == DnsRecordType::TXT {
|
||||||
|
// TXT records have character-strings length-prefixed
|
||||||
|
// We split into chunks of up to 255 bytes
|
||||||
|
let mut txt_data = Vec::new();
|
||||||
|
for chunk in a.rdata.chunks(255) {
|
||||||
|
txt_data.push(chunk.len() as u8);
|
||||||
|
txt_data.extend_from_slice(chunk);
|
||||||
|
}
|
||||||
|
let _ = buf.write_u16::<BigEndian>(txt_data.len() as u16);
|
||||||
|
buf.extend_from_slice(&txt_data);
|
||||||
|
} else {
|
||||||
|
let _ = buf.write_u16::<BigEndian>(a.rdata.len() as u16);
|
||||||
|
buf.extend_from_slice(&a.rdata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: &[u8]) -> Option<Self> {
|
||||||
|
if data.len() < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(data);
|
||||||
|
let id = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let flags = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let qdcount = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let ancount = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let _nscount = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let _arcount = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
|
||||||
|
let mut questions = Vec::new();
|
||||||
|
for _ in 0..qdcount {
|
||||||
|
let name = decode_domain_name(&mut cursor, data)?;
|
||||||
|
let qtype = cursor.read_u16::<BigEndian>().ok()?.into();
|
||||||
|
let qclass = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
questions.push(DnsQuestion { name, qtype, qclass });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut answers = Vec::new();
|
||||||
|
for _ in 0..ancount {
|
||||||
|
let name = decode_domain_name(&mut cursor, data)?;
|
||||||
|
let rtype: DnsRecordType = cursor.read_u16::<BigEndian>().ok()?.into();
|
||||||
|
let rclass = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
let ttl = cursor.read_u32::<BigEndian>().ok()?;
|
||||||
|
let rdlength = cursor.read_u16::<BigEndian>().ok()?;
|
||||||
|
|
||||||
|
let mut rdata = vec![0u8; rdlength as usize];
|
||||||
|
cursor.read_exact(&mut rdata).ok()?;
|
||||||
|
|
||||||
|
if rtype == DnsRecordType::TXT {
|
||||||
|
// Decode TXT string chunks
|
||||||
|
let mut decoded_txt = Vec::new();
|
||||||
|
let mut txt_cursor = Cursor::new(&rdata);
|
||||||
|
while txt_cursor.position() < rdata.len() as u64 {
|
||||||
|
if let Ok(len) = txt_cursor.read_u8() {
|
||||||
|
let mut chunk = vec![0u8; len as usize];
|
||||||
|
if txt_cursor.read_exact(&mut chunk).is_ok() {
|
||||||
|
decoded_txt.extend_from_slice(&chunk);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rdata = decoded_txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
answers.push(DnsAnswer {
|
||||||
|
name,
|
||||||
|
rtype,
|
||||||
|
rclass,
|
||||||
|
ttl,
|
||||||
|
rdata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip authority and additional sections (not needed for basic payload extraction)
|
||||||
|
|
||||||
|
Some(DnsPacket {
|
||||||
|
id,
|
||||||
|
flags,
|
||||||
|
questions,
|
||||||
|
answers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_domain_name(buf: &mut Vec<u8>, name: &str) {
|
||||||
|
for part in name.split('.') {
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let len = part.len().min(63) as u8;
|
||||||
|
buf.push(len);
|
||||||
|
buf.extend_from_slice(&part.as_bytes()[..len as usize]);
|
||||||
|
}
|
||||||
|
buf.push(0); // Root label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_domain_name(cursor: &mut Cursor<&[u8]>, full_data: &[u8]) -> Option<String> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
let mut jumps = 0;
|
||||||
|
let mut current_pos = cursor.position();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if jumps > 100 {
|
||||||
|
return None; // Prevent infinite loops
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_pos >= full_data.len() as u64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = full_data[current_pos as usize];
|
||||||
|
if len == 0 {
|
||||||
|
if jumps == 0 {
|
||||||
|
cursor.set_position(current_pos + 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if len & 0xC0 == 0xC0 {
|
||||||
|
// Pointer
|
||||||
|
if current_pos + 1 >= full_data.len() as u64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pointer = (((len & 0x3F) as u16) << 8) | (full_data[current_pos as usize + 1] as u16);
|
||||||
|
if jumps == 0 {
|
||||||
|
cursor.set_position(current_pos + 2);
|
||||||
|
}
|
||||||
|
jumps += 1;
|
||||||
|
current_pos = pointer as u64;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_pos += 1;
|
||||||
|
if current_pos + len as u64 > full_data.len() as u64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let part = &full_data[current_pos as usize..(current_pos + len as u64) as usize];
|
||||||
|
parts.push(String::from_utf8_lossy(part).into_owned());
|
||||||
|
current_pos += len as u64;
|
||||||
|
|
||||||
|
if jumps == 0 {
|
||||||
|
cursor.set_position(current_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
Some(".".to_string())
|
||||||
|
} else {
|
||||||
|
Some(parts.join("."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a payload into a list of subdomain labels and appends the base domain.
|
||||||
|
/// Each label is max 63 chars. The base32 string is chunked.
|
||||||
|
pub fn encode_payload_to_domain(payload: &[u8], base_domain: &str) -> String {
|
||||||
|
let encoded = base32_encode(payload);
|
||||||
|
let mut domain = String::new();
|
||||||
|
|
||||||
|
let mut start = 0;
|
||||||
|
while start < encoded.len() {
|
||||||
|
let end = (start + 63).min(encoded.len());
|
||||||
|
domain.push_str(&encoded[start..end]);
|
||||||
|
domain.push('.');
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.push_str(base_domain);
|
||||||
|
domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a payload from a subdomain string, ignoring the base domain.
|
||||||
|
pub fn decode_domain_to_payload(full_domain: &str, base_domain: &str) -> Option<Vec<u8>> {
|
||||||
|
// Strip base domain and trailing dots
|
||||||
|
let stripped = full_domain
|
||||||
|
.trim_end_matches('.')
|
||||||
|
.strip_suffix(base_domain)?;
|
||||||
|
|
||||||
|
let stripped = stripped.trim_end_matches('.');
|
||||||
|
|
||||||
|
let mut base32_str = String::with_capacity(stripped.len());
|
||||||
|
for part in stripped.split('.') {
|
||||||
|
base32_str.push_str(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
base32_decode(&base32_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_base32() {
|
||||||
|
let data = b"Hello, OSTP DNS Tunnel!";
|
||||||
|
let encoded = base32_encode(data);
|
||||||
|
let decoded = base32_decode(&encoded).unwrap();
|
||||||
|
assert_eq!(data.as_ref(), decoded.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_domain_encoding() {
|
||||||
|
let payload = vec![0x12; 20];
|
||||||
|
let base_domain = "tunnel.example.com";
|
||||||
|
let domain = encode_payload_to_domain(&payload, base_domain);
|
||||||
|
|
||||||
|
// Ensure no label is > 63 chars
|
||||||
|
for part in domain.split('.') {
|
||||||
|
assert!(part.len() <= 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(domain.ends_with(base_domain));
|
||||||
|
|
||||||
|
let decoded = decode_domain_to_payload(&domain, base_domain).unwrap();
|
||||||
|
assert_eq!(payload, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dns_packet() {
|
||||||
|
let payload = vec![1, 2, 3, 4, 5];
|
||||||
|
let domain = encode_payload_to_domain(&payload, "t.com");
|
||||||
|
|
||||||
|
let query = DnsPacket::new_query(1234, &domain, DnsRecordType::TXT);
|
||||||
|
let encoded_query = query.encode();
|
||||||
|
|
||||||
|
let decoded_query = DnsPacket::decode(&encoded_query).unwrap();
|
||||||
|
assert_eq!(decoded_query.id, 1234);
|
||||||
|
assert_eq!(decoded_query.questions[0].name, domain);
|
||||||
|
assert_eq!(decoded_query.questions[0].qtype, DnsRecordType::TXT);
|
||||||
|
|
||||||
|
let response_data = vec![5, 4, 3, 2, 1];
|
||||||
|
let response = DnsPacket::new_response(1234, &domain, DnsRecordType::TXT, response_data.clone());
|
||||||
|
let encoded_resp = response.encode();
|
||||||
|
|
||||||
|
let decoded_resp = DnsPacket::decode(&encoded_resp).unwrap();
|
||||||
|
assert_eq!(decoded_resp.answers[0].rdata, response_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod framing;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
pub mod resumption;
|
pub mod resumption;
|
||||||
|
pub mod dns;
|
||||||
|
|
||||||
pub use crypto::NoiseRole;
|
pub use crypto::NoiseRole;
|
||||||
pub use framing::{TrafficProfile, PaddingStrategy};
|
pub use framing::{TrafficProfile, PaddingStrategy};
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
late TextEditingController _domainsCtrl;
|
late TextEditingController _domainsCtrl;
|
||||||
late TextEditingController _ipsCtrl;
|
late TextEditingController _ipsCtrl;
|
||||||
late TextEditingController _processesCtrl;
|
late TextEditingController _processesCtrl;
|
||||||
late TextEditingController _stealthSniCtrl;
|
late TextEditingController _dnsDomainCtrl;
|
||||||
late TextEditingController _pbkCtrl;
|
late TextEditingController _pbkCtrl;
|
||||||
late TextEditingController _sidCtrl;
|
late TextEditingController _sidCtrl;
|
||||||
|
|
||||||
bool _obscureKey = true;
|
bool _obscureKey = true;
|
||||||
bool _debugMode = false;
|
bool _debugMode = false;
|
||||||
bool _wss = false;
|
String _dnsRegion = 'Global';
|
||||||
String _transportMode = 'udp'; // 'udp' | 'uot'
|
String _transportMode = 'udp'; // 'udp' | 'uot'
|
||||||
String _tunStack = 'ostp'; // 'system' | 'ostp'
|
String _tunStack = 'ostp'; // 'system' | 'ostp'
|
||||||
bool _muxEnabled = false;
|
bool _muxEnabled = false;
|
||||||
|
|
@ -57,10 +57,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
_domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
|
_domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
|
||||||
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
|
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
|
||||||
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
|
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
|
||||||
_stealthSniCtrl = TextEditingController(text: widget.prefs.getString('stealth_sni') ?? '');
|
_dnsDomainCtrl = TextEditingController(text: widget.prefs.getString('dns_domain') ?? '');
|
||||||
_pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? '');
|
_pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? '');
|
||||||
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
|
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
|
||||||
_wss = widget.prefs.getBool('wss') ?? false;
|
_dnsRegion = widget.prefs.getString('dns_region') ?? 'Global';
|
||||||
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
||||||
_tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
|
_tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
|
||||||
_debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
_debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
||||||
|
|
@ -80,7 +80,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
_domainsCtrl.dispose();
|
_domainsCtrl.dispose();
|
||||||
_ipsCtrl.dispose();
|
_ipsCtrl.dispose();
|
||||||
_processesCtrl.dispose();
|
_processesCtrl.dispose();
|
||||||
_stealthSniCtrl.dispose();
|
_dnsDomainCtrl.dispose();
|
||||||
_pbkCtrl.dispose();
|
_pbkCtrl.dispose();
|
||||||
_sidCtrl.dispose();
|
_sidCtrl.dispose();
|
||||||
_muxSessionsCtrl.dispose();
|
_muxSessionsCtrl.dispose();
|
||||||
|
|
@ -97,10 +97,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
|
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
|
||||||
widget.prefs.setString('ex_processes', _processesCtrl.text.trim());
|
widget.prefs.setString('ex_processes', _processesCtrl.text.trim());
|
||||||
widget.prefs.setBool('debug_mode', _debugMode);
|
widget.prefs.setBool('debug_mode', _debugMode);
|
||||||
widget.prefs.setBool('wss', _wss);
|
widget.prefs.setString('dns_region', _dnsRegion);
|
||||||
widget.prefs.setString('transport_mode', _transportMode);
|
widget.prefs.setString('transport_mode', _transportMode);
|
||||||
widget.prefs.setString('tun_stack', _tunStack);
|
widget.prefs.setString('tun_stack', _tunStack);
|
||||||
widget.prefs.setString('stealth_sni', _stealthSniCtrl.text.trim());
|
widget.prefs.setString('dns_domain', _dnsDomainCtrl.text.trim());
|
||||||
widget.prefs.setString('pbk', _pbkCtrl.text.trim());
|
widget.prefs.setString('pbk', _pbkCtrl.text.trim());
|
||||||
widget.prefs.setString('sid', _sidCtrl.text.trim());
|
widget.prefs.setString('sid', _sidCtrl.text.trim());
|
||||||
widget.prefs.setBool('mux_enabled', _muxEnabled);
|
widget.prefs.setBool('mux_enabled', _muxEnabled);
|
||||||
|
|
@ -236,12 +236,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_serverCtrl.text = host;
|
_serverCtrl.text = host;
|
||||||
_keyCtrl.text = key;
|
_keyCtrl.text = key;
|
||||||
_stealthSniCtrl.text = uri.queryParameters['sni'] ?? '';
|
_dnsDomainCtrl.text = uri.queryParameters['domain'] ?? '';
|
||||||
_pbkCtrl.text = uri.queryParameters['pbk'] ?? '';
|
_dnsRegion = uri.queryParameters['region'] ?? 'Global';
|
||||||
_sidCtrl.text = uri.queryParameters['sid'] ?? '';
|
|
||||||
_wss = uri.queryParameters['wss'] == 'true';
|
final type = uri.queryParameters['type'];
|
||||||
final type = uri.queryParameters['type'] ?? 'udp';
|
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : (type == 'dns' ? 'dns' : 'udp');
|
||||||
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : 'udp';
|
|
||||||
_importCtrl.clear();
|
_importCtrl.clear();
|
||||||
|
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
|
|
@ -292,8 +291,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
RadioListTile<String>(
|
RadioListTile<String>(
|
||||||
value: 'udp',
|
value: 'udp',
|
||||||
groupValue: _transportMode,
|
groupValue: _transportMode,
|
||||||
title: const Text('UDP (по умолчанию)', style: TextStyle(fontWeight: FontWeight.w600)),
|
title: const Text('UDP (Default)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
subtitle: const Text('Быстро, работает через Wi-Fi и большинство сетей', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
subtitle: const Text('Fast, works on Wi-Fi and most networks', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||||
activeColor: Theme.of(context).colorScheme.secondary,
|
activeColor: Theme.of(context).colorScheme.secondary,
|
||||||
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
||||||
),
|
),
|
||||||
|
|
@ -301,111 +300,86 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
RadioListTile<String>(
|
RadioListTile<String>(
|
||||||
value: 'uot',
|
value: 'uot',
|
||||||
groupValue: _transportMode,
|
groupValue: _transportMode,
|
||||||
title: Wrap(
|
title: const Text('UoT (UDP-over-TCP)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
subtitle: const Text('Masks as HTTP stream, bypasses whitelists', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
const Text('UoT (UDP-over-TCP)', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF6C72FF).withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: const Text('xHTTP Стелс', style: TextStyle(fontSize: 10, color: Color(0xFF6C72FF), fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: const Text('Маскировка под HTTP-поток, обходит белые списки (уровень 1)', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
||||||
),
|
),
|
||||||
|
Divider(color: Colors.white.withOpacity(0.05), height: 1),
|
||||||
|
RadioListTile<String>(
|
||||||
|
value: 'dns',
|
||||||
|
groupValue: _transportMode,
|
||||||
|
title: const Text('DNS Proxy (Last Resort)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
subtitle: const Text('Very slow, but works under strict DPI blocks', style: TextStyle(color: Colors.orangeAccent, fontSize: 12)),
|
||||||
|
activeColor: Colors.orangeAccent,
|
||||||
|
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildToggle('WebSocket (WSS)', 'Инкапсулировать транспорт в RFC 6455 (для строгого DPI)', _wss, (val) {
|
|
||||||
setState(() {
|
// DNS Proxy parameters
|
||||||
_wss = val;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Stealth parameters
|
|
||||||
AnimatedCrossFade(
|
AnimatedCrossFade(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
crossFadeState: _transportMode == 'uot' ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
crossFadeState: _transportMode == 'dns' ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
firstChild: Container(
|
firstChild: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF6C72FF).withOpacity(0.06),
|
color: Colors.orangeAccent.withOpacity(0.06),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFF6C72FF).withOpacity(0.2)),
|
border: Border.all(color: Colors.orangeAccent.withOpacity(0.2)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.security, size: 16, color: Color(0xFF6C72FF)),
|
const Icon(Icons.dns, size: 16, color: Colors.orangeAccent),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text('Стелс параметры', style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF6C72FF), fontSize: 14)),
|
const Text('DNS Proxy Settings', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orangeAccent, fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
const Text(
|
||||||
'Укажи домен из белого списка. OSTP подключится к серверу и подделает SNI / HTTP Host.',
|
'Specify the domain pointing to your server. Details in Wiki.',
|
||||||
style: TextStyle(fontSize: 12, color: Colors.white38),
|
style: TextStyle(fontSize: 12, color: Colors.white38),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Builder(builder: (context) {
|
_buildTextField('Domain (Points to Server)', _dnsDomainCtrl, hint: 'tunnel.myvpn.com'),
|
||||||
final List<String> domains = [
|
const SizedBox(height: 16),
|
||||||
'yastatic.net', 'mc.yandex.ru', 'st.mycdn.me',
|
DropdownButtonFormField<String>(
|
||||||
'top-fwz1.mail.ru', 'sso.passport.yandex.ru',
|
value: _dnsRegion,
|
||||||
'sberbank.ru', 'ad.mail.ru', 'ads.vk.com',
|
dropdownColor: const Color(0xFF1E1E2C),
|
||||||
'login.vk.com', 'api.sberbank.ru', 'ok.ru',
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
'rostelecom.ru', 'rt.ru', 'tinkoff.ru',
|
decoration: InputDecoration(
|
||||||
'x5.ru', 'ozon.ru', 'wildberries.ru', 'gosuslugi.ru', 'vk.com'
|
labelText: 'DNS Resolver Region',
|
||||||
];
|
labelStyle: const TextStyle(color: Colors.white54, fontSize: 13),
|
||||||
String currentVal = _stealthSniCtrl.text.trim();
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
if (currentVal.isEmpty) currentVal = 'vk.com';
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
if (!domains.contains(currentVal)) {
|
),
|
||||||
domains.add(currentVal);
|
items: ['Global', 'Russia', 'China', 'Iran'].map((String region) {
|
||||||
}
|
return DropdownMenuItem<String>(
|
||||||
return DropdownButtonFormField<String>(
|
value: region,
|
||||||
value: currentVal,
|
child: Text(region),
|
||||||
dropdownColor: const Color(0xFF1E1E2C),
|
);
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
}).toList(),
|
||||||
decoration: InputDecoration(
|
onChanged: (String? newValue) {
|
||||||
labelText: 'Стелс Домен (Автоподставление)',
|
if (newValue != null) {
|
||||||
labelStyle: const TextStyle(color: Colors.white54, fontSize: 13),
|
setState(() {
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
_dnsRegion = newValue;
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
_saveSettings();
|
||||||
),
|
});
|
||||||
items: domains.map((String domain) {
|
}
|
||||||
return DropdownMenuItem<String>(
|
},
|
||||||
value: domain,
|
),
|
||||||
child: Text(domain),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (String? newValue) {
|
|
||||||
if (newValue != null) {
|
|
||||||
setState(() {
|
|
||||||
_stealthSniCtrl.text = newValue;
|
|
||||||
_saveSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
secondChild: const SizedBox.shrink(),
|
secondChild: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)),
|
_buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)),
|
||||||
AnimatedCrossFade(
|
AnimatedCrossFade(
|
||||||
|
|
@ -552,8 +526,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
if (host.isEmpty || key.isEmpty) return '';
|
if (host.isEmpty || key.isEmpty) return '';
|
||||||
|
|
||||||
final queryParams = <String>[];
|
final queryParams = <String>[];
|
||||||
if (_stealthSniCtrl.text.trim().isNotEmpty) {
|
if (_dnsDomainCtrl.text.trim().isNotEmpty) {
|
||||||
queryParams.add('sni=${Uri.encodeComponent(_stealthSniCtrl.text.trim())}');
|
queryParams.add('domain=${Uri.encodeComponent(_dnsDomainCtrl.text.trim())}');
|
||||||
|
}
|
||||||
|
if (_dnsRegion != 'Global') {
|
||||||
|
queryParams.add('region=${Uri.encodeComponent(_dnsRegion)}');
|
||||||
}
|
}
|
||||||
if (_pbkCtrl.text.trim().isNotEmpty) {
|
if (_pbkCtrl.text.trim().isNotEmpty) {
|
||||||
queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}');
|
queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}');
|
||||||
|
|
@ -561,9 +538,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
if (_sidCtrl.text.trim().isNotEmpty) {
|
if (_sidCtrl.text.trim().isNotEmpty) {
|
||||||
queryParams.add('sid=${Uri.encodeComponent(_sidCtrl.text.trim())}');
|
queryParams.add('sid=${Uri.encodeComponent(_sidCtrl.text.trim())}');
|
||||||
}
|
}
|
||||||
if (_wss) {
|
|
||||||
queryParams.add('wss=true');
|
|
||||||
}
|
|
||||||
if (_transportMode != 'udp') {
|
if (_transportMode != 'udp') {
|
||||||
queryParams.add('type=$_transportMode');
|
queryParams.add('type=$_transportMode');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,18 @@ const translations = {
|
||||||
label_transport: 'Transport Protocol',
|
label_transport: 'Transport Protocol',
|
||||||
label_mtu: 'MTU Size',
|
label_mtu: 'MTU Size',
|
||||||
label_transport: 'Transport Protocol',
|
label_transport: 'Transport Protocol',
|
||||||
label_sni: 'Stealth SNI (Fake Host)',
|
opt_udp: 'UDP (Default)',
|
||||||
|
opt_uot: 'TCP (UoT)',
|
||||||
|
opt_dns: 'DNS Proxy (Last Resort)',
|
||||||
|
label_dns_domain: 'Domain (Points to Server)',
|
||||||
|
dns_domain_hint: 'This is the "last resort" over public DNS servers. You need a domain pointing to your server (NS/A record).',
|
||||||
|
dns_guide: 'Detailed setup guide available in',
|
||||||
|
wiki_link: 'GitHub Wiki',
|
||||||
|
label_dns_region: 'DNS Resolver Region (Prober)',
|
||||||
|
opt_global: 'Global (Cloudflare, Google, etc)',
|
||||||
|
opt_russia: 'Russia (Yandex, VK, etc)',
|
||||||
|
opt_china: 'China (AliDNS, DNSPod, etc)',
|
||||||
|
opt_iran: 'Iran (Shatel, Electro, etc)',
|
||||||
|
|
||||||
label_mtu: 'MTU Size',
|
label_mtu: 'MTU Size',
|
||||||
label_mux: 'Multiplexing (Mux)',
|
label_mux: 'Multiplexing (Mux)',
|
||||||
|
|
@ -90,7 +101,18 @@ const translations = {
|
||||||
label_transport: 'Транспортный протокол',
|
label_transport: 'Транспортный протокол',
|
||||||
label_mtu: 'Размер MTU',
|
label_mtu: 'Размер MTU',
|
||||||
label_transport: 'Транспортный протокол',
|
label_transport: 'Транспортный протокол',
|
||||||
label_sni: 'Маскировочный SNI',
|
opt_udp: 'UDP (по умолчанию)',
|
||||||
|
opt_uot: 'TCP (UoT)',
|
||||||
|
opt_dns: 'DNS Proxy (Последний рубеж)',
|
||||||
|
label_dns_domain: 'Домен (указывает на сервер)',
|
||||||
|
dns_domain_hint: 'Это "последний рубеж" через публичные DNS сервера. Для работы нужен домен, указывающий на ваш сервер (NS/A запись).',
|
||||||
|
dns_guide: 'Подробный гайд по настройке доступен в',
|
||||||
|
wiki_link: 'Wiki на GitHub',
|
||||||
|
label_dns_region: 'Регион DNS Резолверов (Prober)',
|
||||||
|
opt_global: 'Global (Cloudflare, Google и др.)',
|
||||||
|
opt_russia: 'Россия (Yandex, VK и др.)',
|
||||||
|
opt_china: 'Китай (AliDNS, DNSPod и др.)',
|
||||||
|
opt_iran: 'Иран (Shatel, Electro и др.)',
|
||||||
|
|
||||||
label_mtu: 'Размер MTU',
|
label_mtu: 'Размер MTU',
|
||||||
label_mux: 'Мультиплексирование (Mux)',
|
label_mux: 'Мультиплексирование (Mux)',
|
||||||
|
|
|
||||||
|
|
@ -196,27 +196,31 @@
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label>
|
<label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label>
|
||||||
<select id="in-transport" class="field-input">
|
<select id="in-transport" class="field-input">
|
||||||
<option value="udp">UDP (Default)</option>
|
<option value="udp" data-i18n="opt_udp">UDP (Default)</option>
|
||||||
<option value="uot">TCP (UoT)</option>
|
<option value="uot" data-i18n="opt_uot">TCP (UoT)</option>
|
||||||
|
<option value="dns" data-i18n="opt_dns">DNS Proxy (Last Resort)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div id="group-dns-proxy" style="display: none;">
|
||||||
<label class="field-label" for="in-stealth-sni" data-i18n="label_sni">Stealth SNI</label>
|
<div class="field-group">
|
||||||
<input id="in-stealth-sni" class="field-input" type="text" placeholder="www.microsoft.com" spellcheck="false" />
|
<label class="field-label" for="in-dns-domain" style="color: var(--c-warning);" data-i18n="label_dns_domain">Domain (Points to Server)</label>
|
||||||
</div>
|
<input id="in-dns-domain" class="field-input" type="text" placeholder="tunnel.myvpn.com" spellcheck="false" />
|
||||||
|
<div style="font-size: 0.8rem; color: var(--c-sub); margin-top: 4px;">
|
||||||
<div class="toggle-row" id="group-wss">
|
<span data-i18n="dns_domain_hint">This is the "last resort" over public DNS servers. You need a domain pointing to your server (NS/A record).</span>
|
||||||
<div class="toggle-text">
|
<span data-i18n="dns_guide">Detailed setup guide available in</span> <a href="https://github.com/ospab/ostp/wiki/DNS-Transport" target="_blank" style="color: var(--c-accent)" data-i18n="wiki_link">GitHub Wiki</a>.
|
||||||
<span class="toggle-name">WebSocket (WSS)</span>
|
</div>
|
||||||
<span class="toggle-hint">Use RFC 6455 framing for strict DPI bypass</span>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field-label" for="in-dns-region" data-i18n="label_dns_region">DNS Resolver Region (Prober)</label>
|
||||||
|
<select id="in-dns-region" class="field-input">
|
||||||
|
<option value="Global" data-i18n="opt_global">Global (Cloudflare, Google, etc)</option>
|
||||||
|
<option value="Russia" data-i18n="opt_russia">Russia (Yandex, VK, etc)</option>
|
||||||
|
<option value="China" data-i18n="opt_china">China (AliDNS, DNSPod, etc)</option>
|
||||||
|
<option value="Iran" data-i18n="opt_iran">Iran (Shatel, Electro, etc)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" id="in-wss" />
|
|
||||||
<span class="toggle-track">
|
|
||||||
<span class="toggle-thumb"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,9 @@ const inDns = $('in-dns');
|
||||||
|
|
||||||
const groupCustomDns = $('group-custom-dns');
|
const groupCustomDns = $('group-custom-dns');
|
||||||
const inTransport = $('in-transport');
|
const inTransport = $('in-transport');
|
||||||
const inSni = $('in-stealth-sni');
|
const groupDnsProxy = $('group-dns-proxy');
|
||||||
const inWss = $('in-wss');
|
const inDnsDomain = $('in-dns-domain');
|
||||||
|
const inDnsRegion = $('in-dns-region');
|
||||||
const inMtu = $('in-mtu');
|
const inMtu = $('in-mtu');
|
||||||
const inTun = $('in-tun-mode');
|
const inTun = $('in-tun-mode');
|
||||||
const inKillSwitch = $('in-kill-switch');
|
const inKillSwitch = $('in-kill-switch');
|
||||||
|
|
@ -56,6 +57,32 @@ const inDebug = $('in-debug');
|
||||||
const inAutoconnect = $('in-autoconnect');
|
const inAutoconnect = $('in-autoconnect');
|
||||||
const inLaunchStartup = $('in-launch-startup');
|
const inLaunchStartup = $('in-launch-startup');
|
||||||
|
|
||||||
|
function bindSettingsInputs() {
|
||||||
|
const ids = [
|
||||||
|
'in-server', 'in-key', 'in-socks', 'in-dns',
|
||||||
|
'in-transport', 'in-dns-domain', 'in-dns-region',
|
||||||
|
'in-mtu', 'in-mux-sessions',
|
||||||
|
'in-tun-mode', 'in-kill-switch', 'in-mux-mode',
|
||||||
|
'in-debug', 'in-autoconnect', 'in-launch-startup'
|
||||||
|
];
|
||||||
|
ids.forEach(id => {
|
||||||
|
const el = $(id);
|
||||||
|
if (el) el.addEventListener('change', scheduleAutoSave);
|
||||||
|
if (el && el.type === 'text') el.addEventListener('input', scheduleAutoSave);
|
||||||
|
if (el && el.type === 'password') el.addEventListener('input', scheduleAutoSave);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inTransport) {
|
||||||
|
inTransport.addEventListener('change', () => {
|
||||||
|
if (inTransport.value === 'dns') {
|
||||||
|
groupDnsProxy.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
groupDnsProxy.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const wintunModal = $('wintun-modal');
|
const wintunModal = $('wintun-modal');
|
||||||
const btnWintunCancel = $('btn-wintun-cancel');
|
const btnWintunCancel = $('btn-wintun-cancel');
|
||||||
const btnWintunOpen = $('btn-wintun-open');
|
const btnWintunOpen = $('btn-wintun-open');
|
||||||
|
|
@ -338,8 +365,13 @@ async function loadConfigIntoForm() {
|
||||||
inServer.value = ostpOut.server ? `${ostpOut.server}:${ostpOut.port || 50000}` : '';
|
inServer.value = ostpOut.server ? `${ostpOut.server}:${ostpOut.port || 50000}` : '';
|
||||||
inKey.value = ostpOut.access_key || '';
|
inKey.value = ostpOut.access_key || '';
|
||||||
inTransport.value = ostpOut.transport?.type || 'udp';
|
inTransport.value = ostpOut.transport?.type || 'udp';
|
||||||
inSni.value = ostpOut.transport?.stealth_sni || '';
|
if (inTransport.value === 'dns') {
|
||||||
inWss.checked = !!ostpOut.transport?.wss;
|
groupDnsProxy.style.display = 'block';
|
||||||
|
inDnsDomain.value = ostpOut.transport?.domain || '';
|
||||||
|
inDnsRegion.value = ostpOut.transport?.resolver || 'Global';
|
||||||
|
} else {
|
||||||
|
groupDnsProxy.style.display = 'none';
|
||||||
|
}
|
||||||
inMux.checked = !!ostpOut.multiplex?.enabled;
|
inMux.checked = !!ostpOut.multiplex?.enabled;
|
||||||
inMuxSessions.value = ostpOut.multiplex?.sessions || '';
|
inMuxSessions.value = ostpOut.multiplex?.sessions || '';
|
||||||
}
|
}
|
||||||
|
|
@ -382,8 +414,11 @@ async function loadConfigIntoForm() {
|
||||||
inKey.value = c.access_key || '';
|
inKey.value = c.access_key || '';
|
||||||
inSocks.value = c.socks5_bind || '127.0.0.1:1088';
|
inSocks.value = c.socks5_bind || '127.0.0.1:1088';
|
||||||
inTransport.value = c.transport?.mode || 'udp';
|
inTransport.value = c.transport?.mode || 'udp';
|
||||||
inSni.value = c.transport?.stealth_sni || '';
|
if (inTransport.value === 'dns') {
|
||||||
inWss.checked = !!c.transport?.wss;
|
groupDnsProxy.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
groupDnsProxy.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
inMtu.value = c.mtu || '';
|
inMtu.value = c.mtu || '';
|
||||||
inTun.checked = !!c.tun?.enable;
|
inTun.checked = !!c.tun?.enable;
|
||||||
|
|
@ -466,8 +501,8 @@ async function handleSave(silent = false) {
|
||||||
access_key: key,
|
access_key: key,
|
||||||
transport: {
|
transport: {
|
||||||
type: inTransport.value,
|
type: inTransport.value,
|
||||||
stealth_sni: inSni.value.trim() || undefined,
|
domain: inTransport.value === 'dns' ? inDnsDomain.value.trim() : undefined,
|
||||||
wss: inWss.checked ? true : undefined
|
resolver: inTransport.value === 'dns' ? inDnsRegion.value : undefined
|
||||||
},
|
},
|
||||||
multiplex: inMux.checked ? {
|
multiplex: inMux.checked ? {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -527,7 +562,8 @@ function handleImport() {
|
||||||
if (!key || !host) throw new Error('Incomplete link parameters');
|
if (!key || !host) throw new Error('Incomplete link parameters');
|
||||||
inServer.value = host;
|
inServer.value = host;
|
||||||
inKey.value = key;
|
inKey.value = key;
|
||||||
inSni.value = url.searchParams.get('sni') || '';
|
inTransport.value = 'udp';
|
||||||
|
groupDnsProxy.style.display = 'none';
|
||||||
|
|
||||||
const type = url.searchParams.get('type');
|
const type = url.searchParams.get('type');
|
||||||
if (type === 'tcp' || type === 'http') inTransport.value = 'uot';
|
if (type === 'tcp' || type === 'http') inTransport.value = 'uot';
|
||||||
|
|
|
||||||
|
|
@ -121,4 +121,17 @@ pub struct ModularServerConfig {
|
||||||
pub dns: Option<DnsConfig>,
|
pub dns: Option<DnsConfig>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub license_key: Option<String>,
|
pub license_key: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub dns_transport: Option<DnsTransportConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct DnsTransportConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
pub listen: String,
|
||||||
|
pub domain: String,
|
||||||
|
pub pubkey: String,
|
||||||
|
pub privkey: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ pub async fn run_server(
|
||||||
fallback_config: Option<FallbackConfig>,
|
fallback_config: Option<FallbackConfig>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
dns_config: Option<dns::DnsConfig>,
|
dns_config: Option<dns::DnsConfig>,
|
||||||
|
dns_transport: Option<crate::config::DnsTransportConfig>,
|
||||||
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();
|
||||||
|
|
@ -319,7 +320,7 @@ pub async fn run_server(
|
||||||
tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started");
|
tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started");
|
||||||
tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms");
|
tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms");
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, router) => {
|
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, router, dns_transport.clone()) => {
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
tracing::error!("Server error: {e}");
|
tracing::error!("Server error: {e}");
|
||||||
}
|
}
|
||||||
|
|
@ -343,6 +344,7 @@ async fn run_server_loop(
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, crate::api::UserMeta>>>,
|
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||||
router: std::sync::Arc<crate::router::Router>,
|
router: std::sync::Arc<crate::router::Router>,
|
||||||
|
dns_transport: Option<crate::config::DnsTransportConfig>,
|
||||||
) -> 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>)>();
|
||||||
|
|
@ -429,6 +431,22 @@ async fn run_server_loop(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(dns_cfg) = dns_transport {
|
||||||
|
if dns_cfg.enabled {
|
||||||
|
let dns_udp_tx = udp_tx.clone();
|
||||||
|
let dns_tcp_map = tcp_map.clone();
|
||||||
|
let dns_ui_tx = ui_event_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::transport::dns::start_dns_transport_server(
|
||||||
|
dns_cfg,
|
||||||
|
dns_udp_tx,
|
||||||
|
dns_tcp_map,
|
||||||
|
dns_ui_tx,
|
||||||
|
).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drop(udp_tx); // Drop the original sender so the channel closes when all tasks end
|
drop(udp_tx); // Drop the original sender so the channel closes when all tasks end
|
||||||
|
|
||||||
if router.debug {
|
if router.debug {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use tokio::sync::{mpsc, RwLock};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use tokio::time::Duration;
|
||||||
|
|
||||||
|
use ostp_core::dns::{DnsPacket, DnsRecordType, decode_domain_to_payload, encode_payload_to_domain};
|
||||||
|
use crate::config::DnsTransportConfig;
|
||||||
|
use crate::UiEvent;
|
||||||
|
|
||||||
|
pub(crate) async fn start_dns_transport_server(
|
||||||
|
config: DnsTransportConfig,
|
||||||
|
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||||
|
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||||
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
|
) {
|
||||||
|
let listen_addr = if config.listen.contains(':') {
|
||||||
|
config.listen.clone()
|
||||||
|
} else {
|
||||||
|
format!("0.0.0.0:{}", config.listen)
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket = match UdpSocket::bind(&listen_addr).await {
|
||||||
|
Ok(s) => Arc::new(s),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("DNS Transport failed to bind to {}: {}", listen_addr, e);
|
||||||
|
let _ = ui_event_tx.send(UiEvent::Log(format!("DNS Transport failed to bind: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("DNS Transport listening on {}", listen_addr);
|
||||||
|
let _ = ui_event_tx.send(UiEvent::Log(format!("DNS Transport listening on {}", listen_addr)));
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; 65535];
|
||||||
|
loop {
|
||||||
|
match socket.recv_from(&mut buf).await {
|
||||||
|
Ok((size, peer)) => {
|
||||||
|
let packet_bytes = buf[..size].to_vec();
|
||||||
|
let udp_tx = udp_tx.clone();
|
||||||
|
let tcp_map = tcp_map.clone();
|
||||||
|
let socket = socket.clone();
|
||||||
|
let base_domain = config.domain.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Some(dns_req) = DnsPacket::decode(&packet_bytes) {
|
||||||
|
if dns_req.questions.is_empty() { return; }
|
||||||
|
let query = &dns_req.questions[0];
|
||||||
|
|
||||||
|
// Check if it's our target domain and it's a TXT or NULL query
|
||||||
|
if (query.qtype == DnsRecordType::TXT || query.qtype == DnsRecordType::NULL) && query.name.ends_with(&base_domain) {
|
||||||
|
// Decode base32 payload
|
||||||
|
if let Some(payload) = decode_domain_to_payload(&query.name, &base_domain) {
|
||||||
|
|
||||||
|
let (resp_tx, mut resp_rx) = mpsc::channel::<Bytes>(10);
|
||||||
|
|
||||||
|
// Insert into tcp_map so Dispatcher routes responses to us
|
||||||
|
tcp_map.write().await.insert(peer, resp_tx);
|
||||||
|
|
||||||
|
// Send payload to dispatcher
|
||||||
|
if udp_tx.send((Bytes::from(payload), peer)).await.is_ok() {
|
||||||
|
// Wait up to 50ms for any responses
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
|
||||||
|
while let Ok(Some(resp)) = tokio::time::timeout(Duration::from_millis(50), resp_rx.recv()).await {
|
||||||
|
responses.push(resp);
|
||||||
|
if responses.len() >= 3 { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from tcp_map
|
||||||
|
tcp_map.write().await.remove(&peer);
|
||||||
|
|
||||||
|
// Build DNS Answer
|
||||||
|
let mut dns_resp = DnsPacket::new_response(dns_req.id, &query.name, query.qtype.clone(), vec![]);
|
||||||
|
dns_resp.answers.clear(); // We'll add our own
|
||||||
|
|
||||||
|
if !responses.is_empty() {
|
||||||
|
for r in responses {
|
||||||
|
dns_resp.answers.push(ostp_core::dns::DnsAnswer {
|
||||||
|
name: query.name.clone(),
|
||||||
|
rtype: query.qtype.clone(),
|
||||||
|
rclass: 1,
|
||||||
|
ttl: 0,
|
||||||
|
rdata: r.to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Empty response
|
||||||
|
dns_resp.answers.push(ostp_core::dns::DnsAnswer {
|
||||||
|
name: query.name.clone(),
|
||||||
|
rtype: query.qtype.clone(),
|
||||||
|
rclass: 1,
|
||||||
|
ttl: 0,
|
||||||
|
rdata: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp_encoded = dns_resp.encode();
|
||||||
|
let _ = socket.send_to(&resp_encoded, peer).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("DNS Transport recv error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub mod uot;
|
pub mod uot;
|
||||||
|
pub mod dns;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# OSTP DNS Transport (Последний Рубеж)
|
||||||
|
|
||||||
|
DNS Transport (DNS Прокси) — это экспериментальный транспорт, который является "последним рубежом" для обхода блокировок. Он используется, когда все остальные протоколы (UDP, UoT/TCP) полностью заблокированы провайдером или DPI.
|
||||||
|
|
||||||
|
Он маскирует весь VPN-трафик под обычные запросы к DNS-серверам (разрешение имен), что делает его блокировку практически невозможной без отключения интернета в целом. OSTP использует запросы типа `TXT` и `NULL` для передачи данных, используя кодировку Base32.
|
||||||
|
|
||||||
|
> **Внимание:** DNS-туннелирование работает значительно медленнее обычных протоколов из-за накладных расходов протокола DNS. Рекомендуется использовать этот режим только когда другие способы не работают.
|
||||||
|
|
||||||
|
## Как это работает?
|
||||||
|
|
||||||
|
Вместо того чтобы отправлять трафик напрямую на ваш сервер, клиент OSTP отправляет стандартный DNS-запрос на **публичные DNS-серверы** (например, 1.1.1.1, 8.8.8.8) или серверы выбранного вами региона (Prober).
|
||||||
|
Публичный сервер-резолвер перенаправляет этот запрос на **ваш сервер** (как на авторитативный DNS-сервер для вашего домена), а ваш сервер отвечает обратно через резолвер.
|
||||||
|
|
||||||
|
Для настройки этого механизма **вам понадобится собственный домен**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Настройка на стороне сервера (Домен)
|
||||||
|
|
||||||
|
Вам необходимо настроить NS-записи вашего домена так, чтобы они указывали на IP-адрес вашего OSTP сервера.
|
||||||
|
|
||||||
|
Например, вы владеете доменом `myvpn.com` и хотите использовать поддомен `t.myvpn.com` для туннеля, а IP вашего сервера — `192.168.1.100`.
|
||||||
|
|
||||||
|
В панели управления вашего DNS-регистратора добавьте следующие записи:
|
||||||
|
|
||||||
|
1. **A-запись:**
|
||||||
|
- Имя (Host): `ns.myvpn.com`
|
||||||
|
- Тип (Type): `A`
|
||||||
|
- Значение (Value): `192.168.1.100` (IP вашего OSTP-сервера)
|
||||||
|
|
||||||
|
2. **NS-запись:**
|
||||||
|
- Имя (Host): `t.myvpn.com`
|
||||||
|
- Тип (Type): `NS`
|
||||||
|
- Значение (Value): `ns.myvpn.com`
|
||||||
|
|
||||||
|
Теперь любой DNS-запрос к поддоменам `t.myvpn.com` будет направляться на ваш сервер (на порт 53).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Настройка сервера OSTP
|
||||||
|
|
||||||
|
В файле конфигурации вашего OSTP сервера (`config.json` или `server.json`) необходимо включить прослушивание порта 53 для DNS транспорта:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "server",
|
||||||
|
"dns_transport": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 53,
|
||||||
|
"domain": "t.myvpn.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Важно:** Для прослушивания порта 53 на Linux обычно требуются root-права. Убедитесь, что сервер запущен с `sudo` или используйте возможности `setcap` для предоставления доступа к порту:
|
||||||
|
> `sudo setcap cap_net_bind_service=+ep /path/to/ostp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Настройка в приложении (Клиент)
|
||||||
|
|
||||||
|
В клиенте OSTP (Desktop GUI или Mobile):
|
||||||
|
|
||||||
|
1. Перейдите в **Settings (Настройки)**.
|
||||||
|
2. В поле **Transport Protocol** выберите `DNS Proxy (Последний рубеж)`.
|
||||||
|
3. Появится поле **Domain (Points to Server)** — введите сюда настроенный поддомен (например, `t.myvpn.com`).
|
||||||
|
4. Поле **DNS Resolver Region** позволяет выбрать, через серверы какой страны/провайдера будет осуществляться маршрутизация пакетов (например, Global, Russia, China, Iran). Клиент (Prober) автоматически найдет наиболее быстрый публичный резолвер в этом регионе.
|
||||||
|
|
||||||
|
## Ограничения и особенности
|
||||||
|
|
||||||
|
- **Скорость:** Из-за размера DNS-пакетов и задержек публичных серверов, максимальная скорость может составлять 1-5 Мбит/с.
|
||||||
|
- **Polling:** Поскольку DNS работает по принципу "Запрос-Ответ", клиент отправляет пустые поллинговые пакеты каждые 2 секунды, чтобы позволить серверу пересылать входящие данные.
|
||||||
|
- **Поддержка DoH/DoT:** В текущей версии запросы к публичным резолверам отправляются в открытом виде (UDP порт 53). В будущих обновлениях будет добавлена поддержка DNS over HTTPS (DoH) для дополнительного слоя защиты от DPI-фильтров.
|
||||||
|
|
@ -4,4 +4,5 @@ This repository contains the documentation and wiki pages for the Ospab Stealth
|
||||||
|
|
||||||
- [Configuration Guide](configuration_guide.md)
|
- [Configuration Guide](configuration_guide.md)
|
||||||
- [API Endpoints](api_endpoints.md)
|
- [API Endpoints](api_endpoints.md)
|
||||||
|
- [DNS Transport (Последний Рубеж)](DNS-Transport.md)
|
||||||
- [v0.3.1 Configuration Migration Guide](../docs/migration_v0_3_1.md)
|
- [v0.3.1 Configuration Migration Guide](../docs/migration_v0_3_1.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
use ostp_core::dns::{DnsPacket, DnsRecordType};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
const PUBLIC_DNS_SERVERS: &[(&str, &str)] = &[
|
||||||
|
// --- Global & US / EU ---
|
||||||
|
("Google Primary", "8.8.8.8"),
|
||||||
|
("Google Sec", "8.8.4.4"),
|
||||||
|
("Cloudflare Pri", "1.1.1.1"),
|
||||||
|
("Cloudflare Sec", "1.0.0.1"),
|
||||||
|
("Quad9 Pri", "9.9.9.9"),
|
||||||
|
("Quad9 Sec", "149.112.112.112"),
|
||||||
|
("OpenDNS Pri", "208.67.222.222"),
|
||||||
|
("OpenDNS Sec", "208.67.220.220"),
|
||||||
|
("AdGuard Pri", "94.140.14.14"),
|
||||||
|
("AdGuard Sec", "94.140.15.15"),
|
||||||
|
("NextDNS", "45.90.28.0"),
|
||||||
|
("NextDNS Sec", "45.90.30.0"),
|
||||||
|
("Neustar Pri", "156.154.70.1"),
|
||||||
|
("Neustar Sec", "156.154.71.1"),
|
||||||
|
("CleanBrowsing", "185.228.168.9"),
|
||||||
|
("Comodo Pri", "8.26.56.26"),
|
||||||
|
("Comodo Sec", "8.20.247.20"),
|
||||||
|
("Level3 Pri", "209.244.0.3"),
|
||||||
|
("Level3 Sec", "209.244.0.4"),
|
||||||
|
("Verisign Pri", "64.6.64.6"),
|
||||||
|
("Verisign Sec", "64.6.65.6"),
|
||||||
|
("SafeDNS", "195.46.39.39"),
|
||||||
|
("Hurricane Pri", "74.82.42.42"),
|
||||||
|
|
||||||
|
// --- Russia ---
|
||||||
|
("Yandex Basic", "77.88.8.8"),
|
||||||
|
("Yandex Basic 2", "77.88.8.1"),
|
||||||
|
("Yandex Safe", "77.88.8.88"),
|
||||||
|
("Yandex Safe 2", "77.88.8.2"),
|
||||||
|
("Yandex Family", "77.88.8.7"),
|
||||||
|
("Yandex Family 2", "77.88.8.3"),
|
||||||
|
("AdGuard RU Pri", "176.103.130.130"),
|
||||||
|
("AdGuard RU Sec", "176.103.130.131"),
|
||||||
|
("SkyDNS Pri", "193.58.251.251"),
|
||||||
|
("SkyDNS Sec", "193.58.251.252"),
|
||||||
|
("Rostelecom Pri", "212.48.193.36"),
|
||||||
|
("Rostelecom Sec", "213.134.192.222"),
|
||||||
|
("MTS DNS", "212.188.4.10"),
|
||||||
|
("Beeline DNS", "217.118.66.243"),
|
||||||
|
("Megafon DNS", "10.255.255.254"),
|
||||||
|
("TTK DNS", "217.23.136.2"),
|
||||||
|
("Selectel DNS", "188.128.128.128"),
|
||||||
|
("Selectel Sec", "188.128.128.129"),
|
||||||
|
("RU-CENTER", "80.252.130.254"),
|
||||||
|
("Mastertel", "217.70.106.5"),
|
||||||
|
|
||||||
|
// --- China ---
|
||||||
|
("AliDNS Pri", "223.5.5.5"),
|
||||||
|
("AliDNS Sec", "223.6.6.6"),
|
||||||
|
("Tencent Pri", "119.29.29.29"),
|
||||||
|
("Tencent Sec", "182.254.116.116"),
|
||||||
|
("Baidu Pri", "180.76.76.76"),
|
||||||
|
("114DNS Pri", "114.114.114.114"),
|
||||||
|
("114DNS Sec", "114.114.115.115"),
|
||||||
|
("CNNIC Pri", "1.2.4.8"),
|
||||||
|
("CNNIC Sec", "210.2.4.8"),
|
||||||
|
("DNSPod Pri", "119.29.29.29"), // Same as Tencent
|
||||||
|
("SDNS Pri", "1.2.4.8"), // Same as CNNIC
|
||||||
|
("OneDNS Pri", "117.50.11.11"),
|
||||||
|
("OneDNS Sec", "52.80.66.66"),
|
||||||
|
("CERNET Pri", "202.112.14.151"),
|
||||||
|
("China Telecom 1", "218.30.118.6"),
|
||||||
|
("China Telecom 2", "61.139.2.69"),
|
||||||
|
("China Unicom 1", "123.125.81.6"),
|
||||||
|
("China Unicom 2", "140.207.198.6"),
|
||||||
|
("China Mobile 1", "211.136.192.6"),
|
||||||
|
("China Mobile 2", "120.196.165.24"),
|
||||||
|
|
||||||
|
// --- Iran ---
|
||||||
|
("Shecan Pri", "178.22.122.100"),
|
||||||
|
("Shecan Sec", "185.51.200.2"),
|
||||||
|
("Electro Pri", "78.157.42.100"),
|
||||||
|
("Electro Sec", "78.157.42.101"),
|
||||||
|
("Radar Pri", "10.202.10.10"),
|
||||||
|
("Radar Sec", "10.202.10.11"),
|
||||||
|
("403.online Pri", "10.202.10.202"),
|
||||||
|
("403.online Sec", "10.202.10.102"),
|
||||||
|
("Begzar Pri", "185.55.226.26"),
|
||||||
|
("Begzar Sec", "185.55.225.25"),
|
||||||
|
("Asiatech Pri", "80.253.210.253"),
|
||||||
|
("Asiatech Sec", "80.253.210.254"),
|
||||||
|
("Shatel Pri", "85.15.1.14"),
|
||||||
|
("Shatel Sec", "85.15.1.15"),
|
||||||
|
("ParsOnline", "188.122.100.100"),
|
||||||
|
("Irancell DNS", "109.122.192.1"),
|
||||||
|
("MCI DNS", "192.168.1.1"), // Note: local router usually, but standard for MCI cellular
|
||||||
|
("Rightel DNS", "5.200.200.200"),
|
||||||
|
("Afranet Pri", "217.218.155.155"),
|
||||||
|
("MobinNet Pri", "5.160.0.1"),
|
||||||
|
("HiWeb Pri", "185.176.64.64"),
|
||||||
|
("Pishgaman", "5.160.25.25"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub async fn run_prober() {
|
||||||
|
println!("Starting DNS resolver prober to find the fastest server for DNS Transport...");
|
||||||
|
println!("{:<15} | {:<15} | {:<10}", "Name", "IP Address", "Latency");
|
||||||
|
println!("{:-<15}-+-{:-<15}-+-{:-<10}", "", "", "");
|
||||||
|
|
||||||
|
let mut best_server = "8.8.8.8";
|
||||||
|
let mut best_latency = Duration::from_secs(10);
|
||||||
|
|
||||||
|
// We send a random TXT query to test DNS resolution time
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let id: u16 = rng.gen();
|
||||||
|
let packet = DnsPacket::new_query(id, "example.com", DnsRecordType::TXT);
|
||||||
|
let payload = packet.encode();
|
||||||
|
|
||||||
|
for (name, ip) in PUBLIC_DNS_SERVERS {
|
||||||
|
let sock = match tokio::net::UdpSocket::bind("0.0.0.0:0").await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if sock.connect(format!("{}:53", ip)).await.is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
if sock.send(&payload).await.is_ok() {
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
match tokio::time::timeout(Duration::from_secs(2), sock.recv(&mut buf)).await {
|
||||||
|
Ok(Ok(_)) => {
|
||||||
|
let latency = start.elapsed();
|
||||||
|
println!("{:<15} | {:<15} | {:<7} ms", name, ip, latency.as_millis());
|
||||||
|
if latency < best_latency {
|
||||||
|
best_latency = latency;
|
||||||
|
best_server = ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("{:<15} | {:<15} | {:<10}", name, ip, "TIMEOUT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("{:<15} | {:<15} | {:<10}", name, ip, "ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:-<15}-+-{:-<15}-+-{:-<10}", "", "", "");
|
||||||
|
println!("Best DNS Server to use for DNS Transport: {} ({} ms)", best_server, best_latency.as_millis());
|
||||||
|
println!("Update your config.json with this resolver IP address to optimize DNS tunneling latency.");
|
||||||
|
}
|
||||||
124
ostp/src/main.rs
124
ostp/src/main.rs
|
|
@ -5,68 +5,75 @@ use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
|
mod dns_prober;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Path to the JSON configuration file
|
/// Path to the JSON configuration file
|
||||||
#[cfg_attr(unix, arg(long, default_value = "/etc/ostp/config.json"))]
|
#[cfg_attr(unix, arg(long, default_value = "/etc/ostp/config.json", help_heading = "Common Commands"))]
|
||||||
#[cfg_attr(windows, arg(long, default_value = "config.json"))]
|
#[cfg_attr(windows, arg(long, default_value = "config.json", help_heading = "Common Commands"))]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
|
||||||
/// Optional mode to initialize the config for (client or server)
|
/// Optional mode to initialize the config for (client or server)
|
||||||
#[arg(short, long)]
|
#[arg(short, long, help_heading = "Common Commands")]
|
||||||
init: Option<String>,
|
init: Option<String>,
|
||||||
|
|
||||||
/// Run the interactive setup wizard
|
/// Run the interactive setup wizard
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Common Commands")]
|
||||||
setup: bool,
|
setup: bool,
|
||||||
|
|
||||||
/// Generate a new secure access key and exit
|
/// Generate a new secure access key and exit
|
||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long, help_heading = "Common Commands")]
|
||||||
generate_key: bool,
|
generate_key: bool,
|
||||||
|
|
||||||
/// Format for generated key (hex, base64)
|
/// Format for generated key (hex, base64)
|
||||||
#[arg(long, default_value = "hex")]
|
#[arg(long, default_value = "hex", help_heading = "Common Commands")]
|
||||||
format: String,
|
format: String,
|
||||||
|
|
||||||
/// Number of keys to generate
|
/// Number of keys to generate
|
||||||
#[arg(short = 'c', long, default_value_t = 1)]
|
#[arg(short = 'c', long, default_value_t = 1, help_heading = "Common Commands")]
|
||||||
count: usize,
|
count: usize,
|
||||||
|
|
||||||
/// Output ready-to-use client sharing links (ostp://...) from the server configuration
|
/// Output ready-to-use client sharing links (ostp://...) from the server configuration
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Server Commands")]
|
||||||
links: bool,
|
links: bool,
|
||||||
|
|
||||||
/// Validate configuration file and exit
|
/// Validate configuration file and exit
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Common Commands")]
|
||||||
check: bool,
|
check: bool,
|
||||||
|
|
||||||
/// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT) to run instantly
|
/// Optional client connection share link (ostp://ACCESS_KEY@HOST:PORT) to run instantly
|
||||||
|
#[arg(help_heading = "Client Commands")]
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
|
|
||||||
/// Uninstall OSTP: stop service, remove binary and configuration files
|
/// Uninstall OSTP: stop service, remove binary and configuration files
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Common Commands")]
|
||||||
uninstall: bool,
|
uninstall: bool,
|
||||||
|
|
||||||
/// Update OSTP: re-run the install script to fetch and install the latest version
|
/// Update OSTP: re-run the install script to fetch and install the latest version
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Common Commands")]
|
||||||
update: bool,
|
update: bool,
|
||||||
|
|
||||||
/// Import a share link (ostp://...) into the configuration file and exit
|
/// Import a share link (ostp://...) into the configuration file and exit
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Client Commands")]
|
||||||
import: Option<String>,
|
import: Option<String>,
|
||||||
|
|
||||||
/// Output shell export commands for proxy (eval $(ostp --proxy-env))
|
/// Output shell export commands for proxy (eval $(ostp --proxy-env))
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Client Commands")]
|
||||||
proxy_env: bool,
|
proxy_env: bool,
|
||||||
|
|
||||||
/// Output shell export commands to clear proxy (eval $(ostp --proxy-env-clear))
|
/// Output shell export commands to clear proxy (eval $(ostp --proxy-env-clear))
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Client Commands")]
|
||||||
proxy_env_clear: bool,
|
proxy_env_clear: bool,
|
||||||
|
|
||||||
/// Force migration of the configuration file to the latest format and exit
|
/// Force migration of the configuration file to the latest format and exit
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Common Commands")]
|
||||||
migrate: bool,
|
migrate: bool,
|
||||||
|
|
||||||
|
/// Run the network prober to find the fastest DNS resolver for the DNS Transport
|
||||||
|
#[arg(long, help_heading = "Client Commands")]
|
||||||
|
prober: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
|
|
@ -90,6 +97,8 @@ fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
let mut tun_enabled = false;
|
let mut tun_enabled = false;
|
||||||
let mut _tun_dns = None;
|
let mut _tun_dns = None;
|
||||||
let mut _wss_enabled = false;
|
let mut _wss_enabled = false;
|
||||||
|
let mut dns_domain = None;
|
||||||
|
let mut dns_pubkey = None;
|
||||||
|
|
||||||
for (k, v) in parsed.query_pairs() {
|
for (k, v) in parsed.query_pairs() {
|
||||||
match &*k {
|
match &*k {
|
||||||
|
|
@ -98,10 +107,25 @@ fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
"tun" => tun_enabled = v == "true",
|
"tun" => tun_enabled = v == "true",
|
||||||
"dns" => _tun_dns = Some(v.into_owned()),
|
"dns" => _tun_dns = Some(v.into_owned()),
|
||||||
"wss" => _wss_enabled = v == "true",
|
"wss" => _wss_enabled = v == "true",
|
||||||
|
"domain" => dns_domain = Some(v.into_owned()),
|
||||||
|
"pubkey" => dns_pubkey = Some(v.into_owned()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut transport_json = serde_json::json!({
|
||||||
|
"type": transport_mode
|
||||||
|
});
|
||||||
|
|
||||||
|
if transport_mode == "dns" {
|
||||||
|
if let Some(d) = dns_domain {
|
||||||
|
transport_json["domain"] = serde_json::json!(d);
|
||||||
|
}
|
||||||
|
if let Some(p) = dns_pubkey {
|
||||||
|
transport_json["pubkey"] = serde_json::json!(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"log": {
|
"log": {
|
||||||
|
|
@ -129,9 +153,7 @@ fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
|
||||||
"server": parsed.host_str().unwrap_or(""),
|
"server": parsed.host_str().unwrap_or(""),
|
||||||
"port": parsed.port().unwrap_or(50000),
|
"port": parsed.port().unwrap_or(50000),
|
||||||
"access_key": access_key,
|
"access_key": access_key,
|
||||||
"transport": {
|
"transport": transport_json,
|
||||||
"type": transport_mode
|
|
||||||
},
|
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"sessions": 1
|
"sessions": 1
|
||||||
|
|
@ -776,31 +798,36 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
|
|
||||||
wizard_step(3, TOTAL, "Service registration");
|
wizard_step(3, TOTAL, "Service registration");
|
||||||
// intentional: step text then daemon call below
|
// intentional: step text then daemon call below
|
||||||
|
let port_str = listen.split(':').last().unwrap_or("50000");
|
||||||
|
let port: u16 = port_str.parse().unwrap_or(50000);
|
||||||
let server_json = serde_json::json!({
|
let server_json = serde_json::json!({
|
||||||
"mode": "server",
|
"mode": "server",
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"log": {
|
"log": {
|
||||||
"level": "info"
|
"level": "info"
|
||||||
},
|
},
|
||||||
"listen": listen,
|
"dns_transport": {
|
||||||
"access_keys": access_keys,
|
|
||||||
"outbound": {
|
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"protocol": "socks5",
|
"listen": "0.0.0.0:53",
|
||||||
"address": "127.0.0.1",
|
"domain": "tunnel.yourdomain.com",
|
||||||
"port": 9050,
|
"pubkey": "",
|
||||||
"default_action": "proxy",
|
"privkey": ""
|
||||||
"rules": []
|
|
||||||
},
|
},
|
||||||
"api": {
|
"inbounds": [
|
||||||
"enabled": false,
|
{
|
||||||
"bind": "0.0.0.0:9090",
|
"type": "ostp",
|
||||||
"webpath": "",
|
"tag": "ostp-in",
|
||||||
"username": "",
|
"listen": "0.0.0.0",
|
||||||
"password_hash": ""
|
"port": port,
|
||||||
},
|
"users": access_keys
|
||||||
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
|
}
|
||||||
"debug": false
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let actual_path = wizard_save_config(config_path, &server_json)?;
|
let actual_path = wizard_save_config(config_path, &server_json)?;
|
||||||
|
|
@ -898,6 +925,13 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
"log": {
|
"log": {
|
||||||
"level": "info"
|
"level": "info"
|
||||||
},
|
},
|
||||||
|
"dns_transport": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen": "0.0.0.0:53",
|
||||||
|
"domain": "tunnel.yourdomain.com",
|
||||||
|
"pubkey": "",
|
||||||
|
"privkey": ""
|
||||||
|
},
|
||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"type": "ostp",
|
"type": "ostp",
|
||||||
|
|
@ -1383,7 +1417,18 @@ async fn run_app() -> Result<()> {
|
||||||
"target": "127.0.0.1:8080"
|
"target": "127.0.0.1:8080"
|
||||||
}},
|
}},
|
||||||
|
|
||||||
"debug": false
|
"debug": false,
|
||||||
|
|
||||||
|
// [WARNING] This is a last-resort transport via public DNS.
|
||||||
|
// It requires a dedicated registered domain with NS records pointing to this server.
|
||||||
|
// Full setup guide: https://github.com/ospab/ostp/wiki/DNS-Tunneling
|
||||||
|
"dns_transport": {{
|
||||||
|
"enabled": false,
|
||||||
|
"listen": "0.0.0.0:53",
|
||||||
|
"domain": "tunnel.example.com",
|
||||||
|
"pubkey": "SERVER_PUBKEY_BASE64_HERE",
|
||||||
|
"privkey": "SERVER_PRIVKEY_BASE64_HERE"
|
||||||
|
}}
|
||||||
}}"#, key)
|
}}"#, key)
|
||||||
} else if mode_str == "relay" {
|
} else if mode_str == "relay" {
|
||||||
r#"{
|
r#"{
|
||||||
|
|
@ -1501,6 +1546,11 @@ async fn run_app() -> Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.prober {
|
||||||
|
dns_prober::run_prober().await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Validate config file existence
|
// Validate config file existence
|
||||||
if !args.config.exists() {
|
if !args.config.exists() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
|
@ -1685,7 +1735,7 @@ async fn run_app() -> Result<()> {
|
||||||
host_port.0.to_string()
|
host_port.0.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config)).await?;
|
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, server_cfg.dns_transport, Some(args.config)).await?;
|
||||||
}
|
}
|
||||||
AppMode::Client(client_cfg) => {
|
AppMode::Client(client_cfg) => {
|
||||||
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue