Skip to content

Commit

Permalink
feat(dns): bind interface on dns connections (#552)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibigbug authored Aug 20, 2024
1 parent 1efd74a commit c74e958
Show file tree
Hide file tree
Showing 23 changed files with 204 additions and 158 deletions.
10 changes: 5 additions & 5 deletions clash/tests/data/config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ socks-port: 8889
mixed-port: 8899

tun:
enable: true
enable: false
device-id: "dev://utun1989"

ipv6: true
Expand Down Expand Up @@ -40,9 +40,9 @@ dns:
# All DNS questions are sent directly to the nameserver, without proxies
# involved. Clash answers the DNS question with the first result gathered.
nameserver:
- 114.114.114.114 # default value
- 1.1.1.1 # default value
- tls://1.1.1.1:853 # DNS over TLS
# - 114.114.114.114 # default value
# - 1.1.1.1#auto # default value
- tls://1.1.1.1:853#en0 # DNS over TLS
# - dhcp://en0 # dns from dhcp

allow-lan: true
Expand Down Expand Up @@ -280,7 +280,7 @@ rule-providers:
rules:
- DOMAIN,google.com,ws-vmess
- DOMAIN-KEYWORD,httpbin,trojan-grpc
- DOMAIN,ipinfo.io,trojan-grpc
- DOMAIN,ipinfo.io,DIRECT
# - RULE-SET,file-provider,trojan
- GEOIP,CN,relay
- DOMAIN-SUFFIX,facebook.com,REJECT
Expand Down
3 changes: 2 additions & 1 deletion clash_lib/src/app/dns/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ impl Config {
}
}

let net = net.parse()?;
nameservers.push(NameServer {
address: addr,
net: net.parse()?,
net,
interface: iface.map(String::from),
});
}
Expand Down
4 changes: 2 additions & 2 deletions clash_lib/src/app/dns/dhcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ async fn listen_dhcp_client(iface: &str) -> io::Result<UdpSocket> {
};

new_udp_socket(
Some(&listen_addr.parse().expect("must parse")),
Some(&Interface::Name(iface.to_string())),
Some(listen_addr.parse().expect("must parse")),
Some(Interface::Name(iface.to_string())),
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
)
Expand Down
109 changes: 71 additions & 38 deletions clash_lib/src/app/dns/dns_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ use std::{

use async_trait::async_trait;

use futures::{future::BoxFuture, TryFutureExt};
use hickory_client::{
client, client::AsyncClient, proto::iocompat::AsyncIoTokioAsStd,
tcp::TcpClientStream, udp::UdpClientStream,
};
use hickory_proto::error::ProtoError;
use hickory_proto::{
error::ProtoError, rustls::tls_client_stream::tls_client_connect_with_future,
};
use rustls::ClientConfig;
use tokio::{sync::RwLock, task::JoinHandle};
use tracing::{info, warn};

use crate::{
common::tls::{self, GLOBAL_ROOT_STORE},
dns::{dhcp::DhcpClient, ThreadSafeDNSClient},
proxy::utils::{new_tcp_stream, new_udp_socket},
};
use hickory_proto::{
h2::HttpsClientStreamBuilder,
op::Message,
rustls::tls_client_connect_with_bind_addr,
xfer::{DnsRequest, DnsRequestOptions, FirstAnswer},
DnsHandle,
};
Expand Down Expand Up @@ -93,28 +96,28 @@ impl Display for DnsConfig {
DnsConfig::Udp(addr, iface) => {
write!(f, "UDP: {}:{} ", addr.ip(), addr.port())?;
if let Some(iface) = iface {
write!(f, "bind: {}", iface)?;
write!(f, "bind: {} ", iface)?;
}
Ok(())
}
DnsConfig::Tcp(addr, iface) => {
write!(f, "TCP: {}:{} ", addr.ip(), addr.port())?;
if let Some(iface) = iface {
write!(f, "bind: {}", iface)?;
write!(f, "bind: {} ", iface)?;
}
Ok(())
}
DnsConfig::Tls(addr, host, iface) => {
write!(f, "TLS: {}:{} ", addr.ip(), addr.port())?;
if let Some(iface) = iface {
write!(f, "bind: {}", iface)?;
write!(f, "bind: {} ", iface)?;
}
write!(f, "host: {}", host)
}
DnsConfig::Https(addr, host, iface) => {
write!(f, "HTTPS: {}:{} ", addr.ip(), addr.port())?;
if let Some(iface) = iface {
write!(f, "bind: {}", iface)?;
write!(f, "bind: {} ", iface)?;
}
write!(f, "host: {}", host)
}
Expand Down Expand Up @@ -317,32 +320,48 @@ async fn dns_stream_builder(
) -> Result<(AsyncClient, JoinHandle<Result<(), ProtoError>>), Error> {
match cfg {
DnsConfig::Udp(addr, iface) => {
let stream =
UdpClientStream::<TokioUdpSocket>::with_bind_addr_and_timeout(
net::SocketAddr::new(addr.ip(), addr.port()),
// TODO: simplify this match
match iface {
Some(Interface::IpAddr(ip)) => Some(SocketAddr::new(*ip, 0)),
_ => None,
},
Duration::from_secs(5),
);
let iface = iface.clone();

let closure = move |_: SocketAddr,
_: SocketAddr|
-> BoxFuture<
'static,
std::io::Result<tokio::net::UdpSocket>,
> {
Box::pin(new_udp_socket(
None,
iface.clone(),
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
))
};
let stream = UdpClientStream::<TokioUdpSocket>::with_creator(
net::SocketAddr::new(addr.ip(), addr.port()),
None,
Duration::from_secs(5),
Arc::new(closure),
);

client::AsyncClient::connect(stream)
.await
.map(|(x, y)| (x, tokio::spawn(y)))
.map_err(|x| Error::DNSError(x.to_string()))
}
DnsConfig::Tcp(addr, iface) => {
let (stream, sender) = TcpClientStream::<
AsyncIoTokioAsStd<TokioTcpStream>,
>::with_bind_addr_and_timeout(
net::SocketAddr::new(addr.ip(), addr.port()),
match iface {
Some(Interface::IpAddr(ip)) => Some(SocketAddr::new(*ip, 0)),
_ => None,
},
Duration::from_secs(5),
);
let fut = new_tcp_stream(
*addr,
iface.clone(),
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
)
.map_ok(AsyncIoTokioAsStd);

let (stream, sender) =
TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::with_future(
fut,
net::SocketAddr::new(addr.ip(), addr.port()),
Duration::from_secs(5),
);

client::AsyncClient::new(stream, sender, None)
.await
Expand All @@ -356,14 +375,23 @@ async fn dns_stream_builder(
.with_no_client_auth();
tls_config.alpn_protocols = vec!["dot".into()];

let (stream, sender) = tls_client_connect_with_bind_addr::<
let fut = new_tcp_stream(
*addr,
iface.clone(),
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
)
.map_ok(AsyncIoTokioAsStd);

let (stream, sender) = tls_client_connect_with_future::<
AsyncIoTokioAsStd<TokioTcpStream>,
BoxFuture<
'static,
std::io::Result<AsyncIoTokioAsStd<TokioTcpStream>>,
>,
>(
Box::pin(fut),
net::SocketAddr::new(addr.ip(), addr.port()),
match iface {
Some(Interface::IpAddr(ip)) => Some(SocketAddr::new(*ip, 0)),
_ => None,
},
host.clone(),
Arc::new(tls_config),
);
Expand Down Expand Up @@ -391,13 +419,18 @@ async fn dns_stream_builder(
.set_certificate_verifier(Arc::new(tls::NoHostnameTlsVerifier));
}

let mut stream_builder =
HttpsClientStreamBuilder::with_client_config(Arc::new(tls_config));
if let Some(Interface::IpAddr(ip)) = iface {
stream_builder.bind_addr(net::SocketAddr::new(*ip, 0));
}
let stream = stream_builder.build::<AsyncIoTokioAsStd<TokioTcpStream>>(
net::SocketAddr::new(addr.ip(), addr.port()),
let fut = new_tcp_stream(
*addr,
iface.clone(),
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
)
.map_ok(AsyncIoTokioAsStd);

let stream = HttpsClientStreamBuilder::build_with_future(
Box::pin(fut),
Arc::new(tls_config),
*addr,
host.clone(),
);

Expand Down
13 changes: 11 additions & 2 deletions clash_lib/src/app/dns/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
dns_client::{DNSNetMode, DnsClient, Opts},
ClashResolver, ThreadSafeDNSClient,
},
proxy::utils::Interface,
proxy::utils::{get_outbound_interface, Interface},
};
use std::sync::Arc;
use tracing::{debug, warn};
Expand Down Expand Up @@ -37,7 +37,16 @@ pub async fn make_clients(
.parse::<u16>()
.unwrap_or_else(|_| panic!("no port for DNS server: {}", s.address)),
net: s.net.to_owned(),
iface: s.interface.as_ref().map(|x| Interface::Name(x.to_owned())),
iface: s
.interface
.as_ref()
.and_then(|x| match x.as_str() {
"auto" => {
get_outbound_interface().map(|x| Interface::Name(x.name))
}
_ => Some(Interface::Name(x.to_owned())),
})
.inspect(|x| debug!("DNS client interface: {:?}", x)),
})
.await
{
Expand Down
4 changes: 2 additions & 2 deletions clash_lib/src/app/dns/resolver/enhanced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{
time::Duration,
};
use tokio::sync::RwLock;
use tracing::{debug, instrument, warn};
use tracing::{debug, error, instrument, warn};

use hickory_proto::{op, rr};

Expand Down Expand Up @@ -212,7 +212,7 @@ impl EnhancedResolver {
async move {
c.exchange(message)
.inspect_err(|x| {
debug!(
error!(
"DNS client {} resolve error: {}",
c.id(),
x.to_string()
Expand Down
2 changes: 1 addition & 1 deletion clash_lib/src/app/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ pub fn setup_logging(
tracing_subscriber::fmt::Layer::new()
.with_ansi(std::io::stdout().is_terminal())
.compact()
.with_target(true)
.with_target(false)
.with_file(true)
.with_line_number(true)
.with_level(true)
Expand Down
7 changes: 5 additions & 2 deletions clash_lib/src/common/errors.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::io;

pub fn new_io_error(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::Other, msg)
pub fn new_io_error<T>(msg: T) -> io::Error
where
T: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::Other, msg.into())
}

pub fn map_io_error<T>(err: T) -> io::Error
Expand Down
17 changes: 13 additions & 4 deletions clash_lib/src/common/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,31 @@ impl Service<Uri> for LocalConnector {
let dns = self.0.clone();

Box::pin(async move {
new_tcp_stream(
dns,
host.as_str(),
let remote_ip = dns
.resolve(host.as_str(), false)
.await
.map_err(|v| std::io::Error::new(std::io::ErrorKind::Other, v))?
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"no dns result",
))?;
let remote_port =
remote.port_u16().unwrap_or(match remote.scheme_str() {
None => 80,
Some(s) => match s {
"http" => 80,
"https" => 443,
_ => panic!("invalid url: {}", remote),
},
}),
});
new_tcp_stream(
(remote_ip, remote_port).into(),
None,
#[cfg(any(target_os = "linux", target_os = "android"))]
None,
)
.await
.map(|x| Box::new(x) as _)
})
}
}
Expand Down
Loading

0 comments on commit c74e958

Please sign in to comment.