mirror of https://github.com/ospab/ostp.git
Remove built-in DNS server and owndns features
This commit is contained in:
parent
7bb7d211fa
commit
9f35caf4ca
|
|
@ -24,6 +24,8 @@ pub struct ClientConfig {
|
|||
pub tun_stack: String,
|
||||
#[serde(default)]
|
||||
pub kill_switch: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gui: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn default_tun_stack() -> String { "system".to_string() }
|
||||
|
|
@ -151,6 +153,7 @@ impl Default for ClientConfig {
|
|||
dns_server: None,
|
||||
tun_stack: "system".to_string(),
|
||||
kill_switch: false,
|
||||
gui: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,6 +183,7 @@ struct RawUnifiedConfig {
|
|||
mux: Option<RawMuxSection>,
|
||||
reality: Option<RawRealitySection>,
|
||||
transport: Option<RawTransportSection>,
|
||||
gui: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -290,6 +294,7 @@ impl ClientConfig {
|
|||
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
|
||||
tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()),
|
||||
kill_switch: raw.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false),
|
||||
gui: raw.gui,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
String _tunStack = 'ostp'; // 'system' | 'ostp'
|
||||
bool _muxEnabled = false;
|
||||
late TextEditingController _muxSessionsCtrl;
|
||||
bool _owndns = false;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -64,8 +64,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
_debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
||||
_muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
||||
_muxSessionsCtrl = TextEditingController(text: widget.prefs.getString('mux_sessions') ?? '2');
|
||||
_owndns = widget.prefs.getBool('owndns') ?? false;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -105,8 +104,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
widget.prefs.setString('sid', _sidCtrl.text.trim());
|
||||
widget.prefs.setBool('mux_enabled', _muxEnabled);
|
||||
widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim());
|
||||
widget.prefs.setBool('owndns', _owndns);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildTextField(String label, TextEditingController controller, {String? hint, bool isPassword = false, int maxLines = 1, bool isMono = false}) {
|
||||
return Column(
|
||||
|
|
@ -240,8 +238,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
_realityEnabled = uri.queryParameters['reality'] == 'true';
|
||||
final type = uri.queryParameters['type'] ?? 'udp';
|
||||
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : 'udp';
|
||||
_owndns = uri.queryParameters['owndns'] == 'true';
|
||||
_importCtrl.clear();
|
||||
|
||||
_saveSettings();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Imported successfully')));
|
||||
|
|
|
|||
|
|
@ -188,22 +188,7 @@
|
|||
<input id="in-socks" class="field-input" type="text" placeholder="127.0.0.1:1088" />
|
||||
</div>
|
||||
|
||||
<!-- Built-in DNS toggle -->
|
||||
<div class="toggle-row">
|
||||
<div class="toggle-text">
|
||||
<span class="toggle-name" data-i18n="label_owndns">Built-in Server DNS</span>
|
||||
<span class="toggle-hint" data-i18n="owndns_hint">Route DNS through the VPN server (10.1.0.1)</span>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="in-owndns" />
|
||||
<span class="toggle-track">
|
||||
<span class="toggle-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Custom DNS (hidden when built-in is ON) -->
|
||||
<div class="field-group" id="group-custom-dns">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="in-dns" data-i18n="label_dns">Custom DNS Server</label>
|
||||
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const inServer = $('in-server');
|
|||
const inKey = $('in-key');
|
||||
const inSocks = $('in-socks');
|
||||
const inDns = $('in-dns');
|
||||
const inOwndns = $('in-owndns');
|
||||
|
||||
const groupCustomDns = $('group-custom-dns');
|
||||
const inTransport = $('in-transport');
|
||||
const inSni = $('in-stealth-sni');
|
||||
|
|
@ -116,10 +116,7 @@ function showToast(msg, variant = '') {
|
|||
}
|
||||
|
||||
// ── DNS & Kill Switch visibility ──────────────────────────────────────────────
|
||||
function updateDnsVisibility() {
|
||||
if (!groupCustomDns || !inOwndns) return;
|
||||
groupCustomDns.style.display = inOwndns.checked ? 'none' : 'block';
|
||||
}
|
||||
|
||||
|
||||
function updateKillSwitchVisibility() {
|
||||
const group = $('group-kill-switch');
|
||||
|
|
@ -295,12 +292,7 @@ async function loadConfigIntoForm() {
|
|||
inMux.checked = !!c.mux?.enabled;
|
||||
inMuxSessions.value = c.mux?.sessions || '';
|
||||
|
||||
// owndns: detect if saved dns is 10.1.0.1
|
||||
const savedDns = c.tun?.dns || '';
|
||||
const isOwndns = savedDns === '10.1.0.1';
|
||||
inOwndns.checked = isOwndns;
|
||||
inDns.value = isOwndns ? '' : savedDns;
|
||||
updateDnsVisibility();
|
||||
inDns.value = c.tun?.dns || '';
|
||||
updateKillSwitchVisibility();
|
||||
|
||||
inDebug.checked = !!c.debug;
|
||||
|
|
@ -386,8 +378,7 @@ async function handleSave(silent = false) {
|
|||
rawConfig.tun.wintun_path = rawConfig.tun.wintun_path || './wintun.dll';
|
||||
rawConfig.tun.ipv4_address = rawConfig.tun.ipv4_address || '10.1.0.2/24';
|
||||
rawConfig.tun.stack = 'ostp';
|
||||
// owndns: if toggle is on, always write 10.1.0.1; otherwise use the custom field
|
||||
rawConfig.tun.dns = inOwndns.checked ? '10.1.0.1' : (inDns.value.trim() || null);
|
||||
rawConfig.tun.dns = inDns.value.trim() || null;
|
||||
|
||||
rawConfig.exclude = {
|
||||
domains: splitLines(inDomains.value),
|
||||
|
|
@ -447,7 +438,6 @@ function togglePeek() {
|
|||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
applyTranslations();
|
||||
setState('disconnected');
|
||||
updateDnsVisibility(); // initialise field visibility from current checkbox state
|
||||
updateKillSwitchVisibility();
|
||||
|
||||
// Event wiring
|
||||
|
|
@ -564,10 +554,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
if (btnThemeToggle) {
|
||||
btnThemeToggle.addEventListener('click', toggleTheme);
|
||||
}
|
||||
inOwndns.addEventListener('change', () => {
|
||||
updateDnsVisibility();
|
||||
scheduleAutoSave();
|
||||
});
|
||||
scheduleAutoSave();
|
||||
inTun.addEventListener('change', () => {
|
||||
updateKillSwitchVisibility();
|
||||
scheduleAutoSave();
|
||||
|
|
|
|||
|
|
@ -244,9 +244,6 @@ pub fn create_api_router(state: ApiState) -> Router {
|
|||
.route("/users/{key}/reset", post(handle_reset_stats))
|
||||
.route("/subscribe/{key}", get(handle_subscribe))
|
||||
.route("/login", post(handle_login))
|
||||
.route("/dns/config", get(handle_get_dns_config).post(handle_post_dns_config))
|
||||
.route("/dns/queries", get(handle_get_dns_queries))
|
||||
.route("/dns/blocklists/refresh", post(handle_refresh_blocklists))
|
||||
.route(
|
||||
"/audit",
|
||||
get(handle_get_audit)
|
||||
|
|
@ -444,38 +441,6 @@ fn save_config_keys(state: &ApiState) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn save_dns_config(state: &ApiState, cfg: &crate::dns::DnsConfig) -> Result<(), String> {
|
||||
let Some(ref path) = state.config_path else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("failed to read config file: {}", e))?;
|
||||
|
||||
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||
let mut content_str = String::new();
|
||||
use std::io::Read;
|
||||
stripped.read_to_string(&mut content_str)
|
||||
.map_err(|e| format!("failed to strip comments: {}", e))?;
|
||||
|
||||
let mut json_val: serde_json::Value = serde_json::from_str(&content_str)
|
||||
.map_err(|e| format!("failed to parse config JSON: {}", e))?;
|
||||
|
||||
if let Some(obj) = json_val.as_object_mut() {
|
||||
let dns_val = serde_json::to_value(cfg).map_err(|e| e.to_string())?;
|
||||
obj.insert("dns".to_string(), dns_val);
|
||||
} else {
|
||||
return Err("config root is not an object".to_string());
|
||||
}
|
||||
|
||||
let new_content = serde_json::to_string_pretty(&json_val)
|
||||
.map_err(|e| format!("failed to serialize config JSON: {}", e))?;
|
||||
|
||||
std::fs::write(path, new_content)
|
||||
.map_err(|e| format!("failed to write config file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -895,68 +860,6 @@ async fn handle_subscribe(
|
|||
}))).into_response()
|
||||
}
|
||||
|
||||
// ── DNS API Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
async fn handle_get_dns_config(
|
||||
State(state): State<ApiState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
if !check_token(&state, &headers) {
|
||||
return api_unauthorized::<serde_json::Value>();
|
||||
}
|
||||
let cfg = state.dns_server.config.read().await.clone();
|
||||
(StatusCode::OK, ApiResponse::success(serde_json::to_value(cfg).unwrap_or_default()))
|
||||
}
|
||||
|
||||
async fn handle_post_dns_config(
|
||||
State(state): State<ApiState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<crate::dns::DnsConfig>,
|
||||
) -> impl IntoResponse {
|
||||
if !check_token(&state, &headers) {
|
||||
return api_unauthorized::<bool>();
|
||||
}
|
||||
// Update in-memory config
|
||||
let should_refresh = body.enabled && !body.adblock_urls.is_empty();
|
||||
{
|
||||
let mut cfg = state.dns_server.config.write().await;
|
||||
*cfg = body.clone();
|
||||
}
|
||||
// Save to disk
|
||||
if let Err(e) = save_dns_config(&state, &body) {
|
||||
tracing::error!("Failed to save DNS config: {}", e);
|
||||
}
|
||||
// Reload blocklists if enabled
|
||||
if should_refresh {
|
||||
let dns = state.dns_server.clone();
|
||||
tokio::spawn(async move { dns.update_blocklists().await; });
|
||||
}
|
||||
(StatusCode::OK, ApiResponse::success(true))
|
||||
}
|
||||
|
||||
async fn handle_get_dns_queries(
|
||||
State(state): State<ApiState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
if !check_token(&state, &headers) {
|
||||
return api_unauthorized::<Vec<serde_json::Value>>();
|
||||
}
|
||||
let queries = state.dns_server.get_queries().await;
|
||||
let data: Vec<serde_json::Value> = queries.iter().map(|q| serde_json::to_value(q).unwrap_or_default()).collect();
|
||||
(StatusCode::OK, ApiResponse::success(data))
|
||||
}
|
||||
|
||||
async fn handle_refresh_blocklists(
|
||||
State(state): State<ApiState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
if !check_token(&state, &headers) {
|
||||
return api_unauthorized::<bool>();
|
||||
}
|
||||
let dns = state.dns_server.clone();
|
||||
tokio::spawn(async move { dns.update_blocklists().await; });
|
||||
(StatusCode::OK, ApiResponse::success(true))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
@ -1082,15 +985,21 @@ async fn handle_put_rules(
|
|||
if !check_token(&state, &headers) { return api_unauthorized(); }
|
||||
|
||||
// Update memory
|
||||
let mut updated_outbound = None;
|
||||
{
|
||||
let mut lock = state.router.outbound_cfg.write().unwrap();
|
||||
if let Some(cfg) = lock.as_mut() {
|
||||
cfg.rules = new_rules.clone();
|
||||
updated_outbound = Some(cfg.clone());
|
||||
} else {
|
||||
return api_error("Outbound routing is not enabled in config");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cfg) = updated_outbound {
|
||||
state.dns_server.update_proxy(Some(&cfg)).await;
|
||||
}
|
||||
|
||||
// Save to config.json
|
||||
if let Some(path) = &state.config_path {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub struct DnsServer {
|
|||
pub config: RwLock<DnsConfig>,
|
||||
adblock_trie: RwLock<HashSet<String>>,
|
||||
query_log: Mutex<VecDeque<DnsQueryLog>>,
|
||||
reqwest_client: reqwest::Client,
|
||||
reqwest_client: RwLock<reqwest::Client>,
|
||||
}
|
||||
|
||||
impl DnsServer {
|
||||
|
|
@ -57,9 +57,7 @@ impl DnsServer {
|
|||
config: RwLock::new(config.clone()),
|
||||
adblock_trie: RwLock::new(HashSet::new()),
|
||||
query_log: Mutex::new(VecDeque::with_capacity(1000)),
|
||||
reqwest_client: reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap_or_default(),
|
||||
reqwest_client: RwLock::new(reqwest::Client::builder().build().unwrap_or_default()),
|
||||
});
|
||||
|
||||
// Загружаем блок-листы при старте если DNS включён
|
||||
|
|
@ -73,6 +71,33 @@ impl DnsServer {
|
|||
server
|
||||
}
|
||||
|
||||
pub async fn update_proxy(&self, outbound: Option<&crate::outbound::OutboundConfig>) {
|
||||
let mut builder = reqwest::Client::builder();
|
||||
if let Some(outbound) = outbound {
|
||||
if outbound.enabled {
|
||||
// Determine if DoH upstream domain matches any proxy rules
|
||||
// We simplify by just setting the proxy for the client if outbound is globally enabled
|
||||
// But we should check if the DoH URL domain matches Proxy.
|
||||
// Since DoH usually goes to 1.1.1.1 or cloudflare-dns.com, if proxy is enabled, we route it.
|
||||
// Better: just route if proxy is enabled and protocol is socks5/http.
|
||||
let proxy_url = match outbound.protocol.as_str() {
|
||||
"socks5" => Some(format!("socks5h://{}:{}", outbound.address, outbound.port)),
|
||||
"http" => Some(format!("http://{}:{}", outbound.address, outbound.port)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(url) = proxy_url {
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&url) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(client) = builder.build() {
|
||||
*self.reqwest_client.write().await = client;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Скачать и обновить все AdBlock-листы.
|
||||
pub async fn update_blocklists(&self) {
|
||||
let urls = {
|
||||
|
|
@ -84,7 +109,8 @@ impl DnsServer {
|
|||
|
||||
for url in &urls {
|
||||
tracing::info!("DNS: downloading AdBlock list from {url}");
|
||||
match self.reqwest_client.get(url).send().await {
|
||||
let client = self.reqwest_client.read().await.clone();
|
||||
match client.get(url).send().await {
|
||||
Ok(resp) => {
|
||||
match resp.text().await {
|
||||
Ok(text) => {
|
||||
|
|
@ -172,7 +198,6 @@ impl DnsServer {
|
|||
60,
|
||||
RData::A(ip.into()),
|
||||
));
|
||||
self.log_query(qname, client_ip.to_string(), false).await;
|
||||
return response.build_bytes_vec().ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +224,6 @@ impl DnsServer {
|
|||
// Возвращаем пустой NXDOMAIN-ответ
|
||||
let mut response = Packet::new_reply(packet.id());
|
||||
response.questions.push(question.clone());
|
||||
self.log_query(qname.clone(), client_ip.to_string(), true).await;
|
||||
tracing::debug!("DNS AdBlock: blocked {qname} for {client_ip}");
|
||||
return response.build_bytes_vec().ok();
|
||||
}
|
||||
|
|
@ -208,7 +232,8 @@ impl DnsServer {
|
|||
// ── Форвардинг через DoH ──────────────────────────────────────────────
|
||||
// Работает и при enabled=true и при intercept_all_port53=true
|
||||
tracing::debug!("DNS: resolving {qname} via DoH for {client_ip}");
|
||||
match self.reqwest_client
|
||||
let client = self.reqwest_client.read().await.clone();
|
||||
match client
|
||||
.post(&doh_url)
|
||||
.header("Content-Type", "application/dns-message")
|
||||
.header("Accept", "application/dns-message")
|
||||
|
|
@ -219,7 +244,6 @@ impl DnsServer {
|
|||
{
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
if let Ok(bytes) = resp.bytes().await {
|
||||
self.log_query(qname, client_ip.to_string(), false).await;
|
||||
return Some(bytes.to_vec());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,14 +248,12 @@ pub async fn run_server(
|
|||
|
||||
// Инициализируем DNS-сервер
|
||||
let dns_cfg = dns_config.unwrap_or_default();
|
||||
// Запускаем UDP listener если dns.enabled=true (полный режим) или intercept_all_port53=true
|
||||
let start_dns_listener = dns_cfg.enabled || dns_cfg.intercept_all_port53;
|
||||
let dns_server = dns::DnsServer::new(dns_cfg);
|
||||
if start_dns_listener {
|
||||
let dns_srv = dns_server.clone();
|
||||
tokio::spawn(async move { dns_srv.run_local_udp_listener().await });
|
||||
}
|
||||
|
||||
let dns_cfg_update = dns_server.clone();
|
||||
let outbound_clone_update = outbound.clone();
|
||||
tokio::spawn(async move {
|
||||
dns_cfg_update.update_proxy(outbound_clone_update.as_ref()).await;
|
||||
});
|
||||
// Initialize Router
|
||||
let router = std::sync::Arc::new(router::Router::new(
|
||||
outbound.clone(),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ pub struct OutboundRule {
|
|||
pub domain_suffix: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub ip_cidr: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub protocol: Option<String>,
|
||||
pub action: OutboundAction,
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ pub async fn connect_target(
|
|||
let connect_timeout = Duration::from_secs(10);
|
||||
if let Some(outbound) = outbound {
|
||||
if outbound.enabled {
|
||||
let action = select_outbound_action(target, outbound, debug).await;
|
||||
let action = select_outbound_action(target, "tcp", outbound, debug).await;
|
||||
if action == OutboundAction::Block {
|
||||
return Err(anyhow::anyhow!("blocked by outbound rule: {}", target));
|
||||
}
|
||||
|
|
@ -66,8 +68,9 @@ pub async fn connect_target(
|
|||
|
||||
// ── Rule matching ────────────────────────────────────────────────────────────
|
||||
|
||||
async fn select_outbound_action(
|
||||
pub async fn select_outbound_action(
|
||||
target: &str,
|
||||
protocol: &str,
|
||||
outbound: &OutboundConfig,
|
||||
debug: bool,
|
||||
) -> OutboundAction {
|
||||
|
|
@ -78,8 +81,15 @@ async fn select_outbound_action(
|
|||
|
||||
let mut matched = None;
|
||||
for rule in &outbound.rules {
|
||||
if let Some(ref rule_proto) = rule.protocol {
|
||||
if !rule_proto.is_empty() && rule_proto.to_lowercase() != protocol {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if rule.domain_suffix.is_empty() && rule.ip_cidr.is_empty() {
|
||||
continue;
|
||||
// Protocol-only rule match
|
||||
matched = Some(rule.action);
|
||||
break;
|
||||
}
|
||||
if match_domain_rule(&host, &rule.domain_suffix) {
|
||||
matched = Some(rule.action);
|
||||
|
|
@ -121,10 +131,7 @@ async fn match_ip_rule(host: &str, port: u16, cidrs: &[String]) -> bool {
|
|||
return parsed.iter().any(|cidr| cidr.contains(&ip));
|
||||
}
|
||||
|
||||
match tokio::net::lookup_host((host, port)).await {
|
||||
Ok(addrs) => addrs.into_iter().any(|addr| parsed.iter().any(|cidr| cidr.contains(&addr.ip()))),
|
||||
Err(_) => false,
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── SOCKS5 / HTTP CONNECT upstream proxy ─────────────────────────────────────
|
||||
|
|
@ -202,8 +209,213 @@ async fn connect_via_http(proxy_addr: &str, target: &str) -> Result<TcpStream> {
|
|||
Ok(stream)
|
||||
}
|
||||
|
||||
pub enum UdpProxySocket {
|
||||
Direct(std::sync::Arc<tokio::net::UdpSocket>),
|
||||
Socks5 {
|
||||
tcp_keepalive: TcpStream,
|
||||
udp_sock: std::sync::Arc<tokio::net::UdpSocket>,
|
||||
proxy_bnd_addr: std::net::SocketAddr,
|
||||
},
|
||||
}
|
||||
|
||||
impl UdpProxySocket {
|
||||
pub async fn send_to(&self, data: &[u8], target: &str) -> Result<usize> {
|
||||
match self {
|
||||
UdpProxySocket::Direct(sock) => {
|
||||
sock.send_to(data, target).await.map_err(Into::into)
|
||||
}
|
||||
UdpProxySocket::Socks5 { udp_sock, proxy_bnd_addr, .. } => {
|
||||
let (host, port) = split_host_port(target).ok_or_else(|| anyhow::anyhow!("invalid target"))?;
|
||||
let mut req = Vec::with_capacity(10 + host.len() + data.len());
|
||||
req.extend_from_slice(&[0x00, 0x00, 0x00]); // RSV, FRAG
|
||||
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
req.push(0x01);
|
||||
req.extend_from_slice(&v4.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
req.push(0x04);
|
||||
req.extend_from_slice(&v6.octets());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
req.push(0x03);
|
||||
req.push(host.len() as u8);
|
||||
req.extend_from_slice(host.as_bytes());
|
||||
}
|
||||
req.extend_from_slice(&port.to_be_bytes());
|
||||
req.extend_from_slice(data);
|
||||
|
||||
udp_sock.send_to(&req, proxy_bnd_addr).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, String)> {
|
||||
match self {
|
||||
UdpProxySocket::Direct(sock) => {
|
||||
let (len, addr) = sock.recv_from(buf).await?;
|
||||
Ok((len, addr.to_string()))
|
||||
}
|
||||
UdpProxySocket::Socks5 { udp_sock, proxy_bnd_addr, .. } => {
|
||||
loop {
|
||||
let (len, src) = udp_sock.recv_from(buf).await?;
|
||||
if src != *proxy_bnd_addr {
|
||||
continue; // ignore rogue packets
|
||||
}
|
||||
if len < 10 {
|
||||
continue;
|
||||
}
|
||||
if buf[0] != 0x00 || buf[1] != 0x00 {
|
||||
continue; // Invalid RSV
|
||||
}
|
||||
let frag = buf[2];
|
||||
if frag != 0x00 {
|
||||
continue; // Fragments not supported
|
||||
}
|
||||
let atyp = buf[3];
|
||||
let (addr_str, port, payload_offset) = match atyp {
|
||||
0x01 if len >= 10 => {
|
||||
let ip = std::net::Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]);
|
||||
let port = u16::from_be_bytes([buf[8], buf[9]]);
|
||||
(ip.to_string(), port, 10)
|
||||
}
|
||||
0x04 if len >= 22 => {
|
||||
let mut ip_bytes = [0u8; 16];
|
||||
ip_bytes.copy_from_slice(&buf[4..20]);
|
||||
let ip = std::net::Ipv6Addr::from(ip_bytes);
|
||||
let port = u16::from_be_bytes([buf[20], buf[21]]);
|
||||
(ip.to_string(), port, 22)
|
||||
}
|
||||
0x03 if len >= 5 => {
|
||||
let domain_len = buf[4] as usize;
|
||||
if len >= 5 + domain_len + 2 {
|
||||
let domain = String::from_utf8_lossy(&buf[5..5 + domain_len]).into_owned();
|
||||
let port = u16::from_be_bytes([buf[5 + domain_len], buf[5 + domain_len + 1]]);
|
||||
(domain, port, 5 + domain_len + 2)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let target = format!("{}:{}", addr_str, port);
|
||||
let payload_len = len - payload_offset;
|
||||
// Move payload to start of buffer
|
||||
buf.copy_within(payload_offset..len, 0);
|
||||
return Ok((payload_len, target));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_udp_target(
|
||||
target: &str,
|
||||
outbound: Option<&OutboundConfig>,
|
||||
debug: bool,
|
||||
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
||||
) -> Result<UdpProxySocket> {
|
||||
if let Some(outbound) = outbound {
|
||||
if outbound.enabled {
|
||||
let action = select_outbound_action(target, "udp", outbound, debug).await;
|
||||
if action == OutboundAction::Block {
|
||||
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
||||
}
|
||||
if action == OutboundAction::Proxy {
|
||||
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
||||
if outbound.protocol == "socks5" {
|
||||
return connect_udp_via_socks5(&proxy_addr, server_udp).await;
|
||||
}
|
||||
// HTTP CONNECT does not support UDP. Fallback to direct.
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(UdpProxySocket::Direct(server_udp))
|
||||
}
|
||||
|
||||
pub async fn connect_udp_via_socks5(
|
||||
proxy_addr: &str,
|
||||
server_udp: std::sync::Arc<tokio::net::UdpSocket>,
|
||||
) -> Result<UdpProxySocket> {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
let mut stream = TcpStream::connect(proxy_addr).await?;
|
||||
stream.write_all(&[0x05, 0x01, 0x00]).await?;
|
||||
let mut reply = [0u8; 2];
|
||||
stream.read_exact(&mut reply).await?;
|
||||
if reply != [0x05, 0x00] {
|
||||
anyhow::bail!("SOCKS5 auth not accepted");
|
||||
}
|
||||
|
||||
// Send UDP Associate request
|
||||
let local_addr = server_udp.local_addr()?;
|
||||
let mut req = vec![0x05, 0x03, 0x00];
|
||||
match local_addr.ip() {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
req.push(0x01);
|
||||
req.extend_from_slice(&v4.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
req.push(0x04);
|
||||
req.extend_from_slice(&v6.octets());
|
||||
}
|
||||
}
|
||||
req.extend_from_slice(&local_addr.port().to_be_bytes());
|
||||
stream.write_all(&req).await?;
|
||||
|
||||
let mut header = [0u8; 4];
|
||||
stream.read_exact(&mut header).await?;
|
||||
if header[1] != 0x00 {
|
||||
anyhow::bail!("SOCKS5 UDP associate failed: 0x{:02x}", header[1]);
|
||||
}
|
||||
|
||||
let bnd_addr = match header[3] {
|
||||
0x01 => {
|
||||
let mut ip = [0u8; 4];
|
||||
stream.read_exact(&mut ip).await?;
|
||||
std::net::IpAddr::V4(ip.into())
|
||||
}
|
||||
0x04 => {
|
||||
let mut ip = [0u8; 16];
|
||||
stream.read_exact(&mut ip).await?;
|
||||
std::net::IpAddr::V6(ip.into())
|
||||
}
|
||||
0x03 => {
|
||||
let mut len = [0u8; 1];
|
||||
stream.read_exact(&mut len).await?;
|
||||
let mut domain = vec![0u8; len[0] as usize];
|
||||
stream.read_exact(&mut domain).await?;
|
||||
let domain_str = String::from_utf8_lossy(&domain);
|
||||
// SOCKS5 specifies BND.ADDR. If it's a domain, we must resolve it.
|
||||
// Typically proxies return an IP address for BND.ADDR.
|
||||
let resolved = tokio::net::lookup_host(format!("{}:0", domain_str))
|
||||
.await?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not resolve proxy BND.ADDR"))?;
|
||||
resolved.ip()
|
||||
}
|
||||
_ => anyhow::bail!("unknown address type in SOCKS5 reply"),
|
||||
};
|
||||
|
||||
let mut port_bytes = [0u8; 2];
|
||||
stream.read_exact(&mut port_bytes).await?;
|
||||
let bnd_port = u16::from_be_bytes(port_bytes);
|
||||
|
||||
let proxy_bnd_addr = std::net::SocketAddr::new(bnd_addr, bnd_port);
|
||||
|
||||
Ok(UdpProxySocket::Socks5 {
|
||||
tcp_keepalive: stream,
|
||||
udp_sock: server_udp,
|
||||
proxy_bnd_addr,
|
||||
})
|
||||
}
|
||||
|
||||
// ── CIDR utilities ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
enum Cidr {
|
||||
V4(u32, u8),
|
||||
V6(u128, u8),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,20 @@ use tokio::sync::mpsc;
|
|||
use crate::dispatcher::Dispatcher;
|
||||
use crate::{RemoteState, UiEvent};
|
||||
|
||||
fn clean_ipv6_mapped_v4(addr: std::net::SocketAddr) -> std::net::SocketAddr {
|
||||
match addr {
|
||||
std::net::SocketAddr::V6(v6) => {
|
||||
if let Some(v4) = v6.ip().to_ipv4() {
|
||||
std::net::SocketAddr::new(std::net::IpAddr::V4(v4), v6.port())
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
_ => addr,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn handle_relay_message(
|
||||
peer_addr: std::net::SocketAddr,
|
||||
session_id: u32,
|
||||
|
|
@ -112,55 +126,60 @@ pub async fn handle_relay_message(
|
|||
}
|
||||
};
|
||||
|
||||
let session_router = std::sync::Arc::new(router.route_udp_associate(server_udp.clone()).await);
|
||||
|
||||
let (udp_tx, mut udp_rx) = mpsc::unbounded_channel::<(String, Bytes)>();
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
|
||||
let (dummy_data_tx, _) = mpsc::unbounded_channel::<Bytes>();
|
||||
|
||||
// Outbound UDP loop (tunnel -> target)
|
||||
let tx_sock = server_udp.clone();
|
||||
let _dns_srv = router.dns_server.clone();
|
||||
let _udp_reply_clone_dns = udp_reply_tx.clone();
|
||||
let _client_ip = peer_addr.ip();
|
||||
let tx_router = session_router.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some((target, data)) = udp_rx.recv().await {
|
||||
let mut forward_target = target.clone();
|
||||
if forward_target.starts_with("10.1.0.1:") {
|
||||
forward_target = forward_target.replace("10.1.0.1:", "127.0.0.1:");
|
||||
}
|
||||
let _ = tx_sock.send_to(&data, &forward_target).await;
|
||||
let _ = tx_router.send_to(&data, &forward_target).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Inbound UDP loop (target -> tunnel)
|
||||
let rx_sock = server_udp.clone();
|
||||
let udp_reply_clone = udp_reply_tx.clone();
|
||||
let proxy_sock = session_router.get_proxy_sock();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; 65536];
|
||||
let mut direct_buf = vec![0u8; 65536];
|
||||
let mut proxy_buf = vec![0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.recv() => break,
|
||||
res = rx_sock.recv_from(&mut buf) => {
|
||||
match res {
|
||||
Ok((len, addr)) => {
|
||||
let clean_addr = match addr {
|
||||
std::net::SocketAddr::V6(v6) => {
|
||||
if let Some(v4) = v6.ip().to_ipv4() {
|
||||
std::net::SocketAddr::new(std::net::IpAddr::V4(v4), v6.port())
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
_ => addr,
|
||||
};
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, clean_addr.to_string(), buf[..len].to_vec()));
|
||||
if let Some(ref p) = proxy_sock {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.recv() => break,
|
||||
res = rx_sock.recv_from(&mut direct_buf) => {
|
||||
if let Ok((len, addr)) = res {
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, clean_ipv6_mapped_v4(addr).to_string(), direct_buf[..len].to_vec()));
|
||||
} else { break; }
|
||||
}
|
||||
res = p.recv_from(&mut proxy_buf) => {
|
||||
if let Ok((len, target_str)) = res {
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, target_str, proxy_buf[..len].to_vec()));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.recv() => break,
|
||||
res = rx_sock.recv_from(&mut direct_buf) => {
|
||||
if let Ok((len, addr)) = res {
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, clean_ipv6_mapped_v4(addr).to_string(), direct_buf[..len].to_vec()));
|
||||
} else { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
remotes.insert((session_id, stream_id), RemoteState {
|
||||
data_tx: dummy_data_tx,
|
||||
udp_tx: Some(udp_tx),
|
||||
|
|
|
|||
|
|
@ -24,13 +24,80 @@ impl Router {
|
|||
pub async fn route_tcp(&self, target: &str) -> Result<TcpStream> {
|
||||
let cfg = {
|
||||
let lock = self.outbound_cfg.read().unwrap();
|
||||
lock.clone() // Clone config to avoid holding lock across await point
|
||||
lock.clone()
|
||||
};
|
||||
connect_target(target, cfg.as_ref(), self.debug).await
|
||||
}
|
||||
|
||||
/// UDP Target Routing
|
||||
pub async fn route_udp(&self, target: &str, server_udp: std::sync::Arc<tokio::net::UdpSocket>) -> Result<crate::outbound::UdpProxySocket> {
|
||||
let cfg = {
|
||||
let lock = self.outbound_cfg.read().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
crate::outbound::connect_udp_target(target, cfg.as_ref(), self.debug, server_udp).await
|
||||
}
|
||||
|
||||
/// Establish a UDP session router that can dynamically route packets
|
||||
pub async fn route_udp_associate(&self, server_udp: std::sync::Arc<tokio::net::UdpSocket>) -> UdpSessionRouter {
|
||||
let cfg = {
|
||||
let lock = self.outbound_cfg.read().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
let mut proxy = None;
|
||||
if let Some(ref c) = cfg {
|
||||
if c.enabled && c.protocol == "socks5" {
|
||||
let proxy_addr = format!("{}:{}", c.address, c.port);
|
||||
if let Ok(p) = crate::outbound::connect_udp_via_socks5(&proxy_addr, server_udp.clone()).await {
|
||||
proxy = Some(Arc::new(p));
|
||||
} else if self.debug {
|
||||
tracing::warn!("Failed to establish SOCKS5 UDP Associate");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UdpSessionRouter {
|
||||
direct: server_udp,
|
||||
proxy,
|
||||
cfg,
|
||||
debug: self.debug,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified DNS Routing and Resolution (AdBlock / Custom Domains / DoH)
|
||||
pub async fn route_dns(&self, client_ip: std::net::IpAddr, payload: &[u8]) -> Option<Vec<u8>> {
|
||||
self.dns_server.resolve(payload, client_ip).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpSessionRouter {
|
||||
direct: Arc<tokio::net::UdpSocket>,
|
||||
proxy: Option<Arc<crate::outbound::UdpProxySocket>>,
|
||||
cfg: Option<OutboundConfig>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl UdpSessionRouter {
|
||||
pub async fn send_to(&self, data: &[u8], target: &str) -> Result<usize> {
|
||||
if let Some(cfg) = &self.cfg {
|
||||
if cfg.enabled {
|
||||
let action = crate::outbound::select_outbound_action(target, "udp", cfg, self.debug).await;
|
||||
if action == crate::outbound::OutboundAction::Block {
|
||||
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
||||
}
|
||||
if action == crate::outbound::OutboundAction::Proxy {
|
||||
if let Some(p) = &self.proxy {
|
||||
return p.send_to(data, target).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.direct.send_to(data, target).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_proxy_sock(&self) -> Option<Arc<crate::outbound::UdpProxySocket>> {
|
||||
self.proxy.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -775,28 +775,7 @@ async fn run_app() -> Result<()> {
|
|||
"sni_list": ["www.microsoft.com"]
|
||||
}},
|
||||
|
||||
// Built-in DNS server
|
||||
"dns": {{
|
||||
// Full mode: custom domains + AdBlock lists + DoH forwarding
|
||||
"enabled": false,
|
||||
// Intercept ALL UDP port 53 traffic and resolve via DoH (prevents DNS leaks through the server)
|
||||
// Works even if enabled=false — just strips AdBlock/custom domains logic
|
||||
"intercept_all_port53": false,
|
||||
// UDP port the built-in DNS server listens on (clients can use <server_ip>:50053 as DNS)
|
||||
"local_port": 50053,
|
||||
// DoH upstream: Cloudflare, Google, NextDNS, etc.
|
||||
"doh_upstream": "https://cloudflare-dns.com/dns-query",
|
||||
// AdBlock lists (hosts format, ||domain^, or one domain per line)
|
||||
"adblock_urls": [
|
||||
// "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
|
||||
// "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
|
||||
],
|
||||
// Custom domains: respond with A record directly (bypasses DoH)
|
||||
"custom_domains": {{
|
||||
// "myserver.internal": "10.0.0.1",
|
||||
// "home.local": "192.168.1.100"
|
||||
}}
|
||||
}},
|
||||
|
||||
"debug": false
|
||||
}}"#, key, priv_key, pub_key, sid)
|
||||
} else if mode_str == "relay" {
|
||||
|
|
@ -991,11 +970,7 @@ async fn run_app() -> Result<()> {
|
|||
query_params.push("type=udp".to_string());
|
||||
}
|
||||
|
||||
if let Some(dns) = &server_cfg.dns {
|
||||
if dns.enabled {
|
||||
query_params.push("owndns=true".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !query_params.is_empty() {
|
||||
link.push('?');
|
||||
|
|
|
|||
24
server.json
24
server.json
|
|
@ -59,28 +59,4 @@
|
|||
},
|
||||
"debug": false,
|
||||
|
||||
// Встроенный DNS-сервер
|
||||
"dns": {
|
||||
// Полный режим: кастомные домены + AdBlock списки + DoH форвардинг
|
||||
"enabled": false,
|
||||
// Перехватывать весь UDP-трафик к порту 53 и резолвить через DoH
|
||||
// (работает даже если enabled=false, предотвращает DNS-утечки через сервер)
|
||||
"intercept_all_port53": false,
|
||||
// УДП порт встроенного DNS (клиент может указать <server_ip>:50053 как DNS)
|
||||
"local_port": 50053,
|
||||
// DoH вверх: Cloudflare, Google, NextDNS и др.
|
||||
"doh_upstream": "https://cloudflare-dns.com/dns-query",
|
||||
// "doh_upstream": "https://dns.google/dns-query",
|
||||
// "doh_upstream": "https://dns10.quad9.net/dns-query",
|
||||
// Списки доменов для блокировки (формат: hosts-файл, ||domain^, один домен на строку)
|
||||
"adblock_urls": [
|
||||
// "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
|
||||
// "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
|
||||
],
|
||||
// Кастомные домены: отвечаем A-записью напрямую (не через DoH)
|
||||
"custom_domains": {
|
||||
// "myserver.internal": "10.0.0.1",
|
||||
// "home.local": "192.168.1.100"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue