Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ client executable in the process-bypass policy. Additional processes can bypass
TUN with repeatable `--tun-bypass-process` options. Use `--tun-list-processes`
to list currently running Windows executable names.

Desktop DNS prefers AliDNS and Tencent DNSPod through the physical interface.
Explicit process exclusions also apply to local HTTP/SOCKS clients using the
system proxy; their hostname lookups and outbound sockets use the physical
interface. See [DNS, exclusions and diagnostic logs](docs/client-usage.md#tun-mode)
for resolver order, platform differences and upgrade behavior.

Windows setup leaves every existing default route untouched and captures IPv4
with two owned `/1` routes. Shutdown and hot switching cancel all managed TUN
sessions before restoring only those exact routes and the previous DNS state;
Expand Down
1 change: 1 addition & 0 deletions crates/proxy-client/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ async fn main() -> Result<()> {
upstream_proxy: upstream_proxy.clone(),
tun,
force_proxy: None,
process_bypass: None,
};
let env_listen_host = std::env::var("LISTEN_HOST").ok();
let listen_host = cli
Expand Down
28 changes: 16 additions & 12 deletions crates/proxy-client/src/client/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use tokio::io::AsyncWriteExt;

use super::{
ForwardContext, ForwarderProvider, HeaderContext, HttpProxySnafu, ProxyContext,
ServerConnection, TcpForwardImpl, connect_native, get_tcp_stream, resolve_server_connection,
split_and_wrap,
ServerConnection, TcpForwardImpl, connect_native, get_tcp_stream, split_and_wrap,
};

#[derive(Debug, Snafu)]
Expand Down Expand Up @@ -251,6 +250,20 @@ impl HttpProxierProvider {
) -> super::Result<(ProxyStream, bool, Option<Cow<'static, str>>)> {
// if we don't have ssl,only use proxy when host is part of `PROXY_KEYWORDS`
if !self.has_ssl {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if context.direct_interface.is_some() {
let mut connection = context.connect(&self.host, self.port).await?;
connection
.stream
.write_all(context.buffer)
.await
.with_context(|_| IoSnafu {
uri: Some(self.get_uri()),
detail: "write process-bypass HTTP request",
})
.context(HttpProxySnafu)?;
return Ok((connection.stream, false, None));
}
// Compiled-in direct hosts win over the upstream proxy and over
// `force_proxy`; see `proxy_core::config::FORCED_DIRECT_HOSTS`.
if context.honor_forced_direct
Expand Down Expand Up @@ -378,16 +391,7 @@ impl HttpProxierProvider {
stream,
need_proxy,
msg_key,
} = match resolve_server_connection(
self.host.as_str(),
self.port,
#[cfg(feature = "auto-proxy")]
context.sender,
context.upstream_proxy,
context.honor_forced_direct,
)
.await
{
} = match context.connect(self.host.as_str(), self.port).await {
Ok(connection) => connection,
Err(error) => {
let _ = context.stream.write_all(
Expand Down
127 changes: 113 additions & 14 deletions crates/proxy-client/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod macos_dns_restore;
pub mod macos_tun;
#[cfg(feature = "network-extension")]
pub mod packet_tunnel;
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
mod process_bypass;
pub mod socks;
pub mod tun;
mod udp;
Expand Down Expand Up @@ -69,6 +71,14 @@ impl Default for ClientConfig {
}
}

#[derive(Debug, Clone, Default)]
pub struct ProcessBypassConfig {
pub processes: tun2proxy::ProcessBypass,
pub virtual_dns: tun::TunVirtualDnsState,
/// Custom resolver used after preferred and physical-adapter resolvers.
pub dns_fallback: Option<std::net::IpAddr>,
}

#[derive(Debug, Clone, Default)]
pub struct ClientRuntimeConfig {
pub client: ClientConfig,
Expand All @@ -80,6 +90,9 @@ pub struct ClientRuntimeConfig {
/// connection uses the remote proxy so client-owned direct sockets cannot
/// be captured and fed back into the listener.
pub force_proxy: Option<Arc<AtomicBool>>,
/// User-selected process rules, shared with a separately managed TUN.
/// The mandatory self bypass is never a local-proxy direct rule.
pub process_bypass: Option<ProcessBypassConfig>,
}

impl From<ClientConfig> for ClientRuntimeConfig {
Expand All @@ -89,6 +102,7 @@ impl From<ClientConfig> for ClientRuntimeConfig {
upstream_proxy: None,
tun: None,
force_proxy: None,
process_bypass: None,
}
}
}
Expand Down Expand Up @@ -119,6 +133,8 @@ pub struct ProxyContext<'a> {
honor_forced_direct: bool,
/// Whether the local listener accepts SOCKS5 UDP ASSOCIATE requests.
enable_udp: bool,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
direct_interface: Option<&'a process_bypass::DirectRoute>,
/// TODO: let this stream abstract
stream: TcpStream,
}
Expand All @@ -136,6 +152,34 @@ pub trait ForwarderProvider {
) -> impl std::future::Future<Output = Result<Self::Item>> + Send;
}

impl ProxyContext<'_> {
async fn connect(&self, host: &str, port: u16) -> Result<ServerConnection> {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if let Some(iface) = self.direct_interface {
let stream = process_bypass::connect(host, port, iface)
.await
.context(IoSnafu {
uri: Some(format!("{host}:{port}")),
detail: "process-bypass direct connection",
})?;
return Ok(ServerConnection {
stream: stream.into(),
need_proxy: false,
msg_key: None,
});
}
resolve_server_connection(
host,
port,
#[cfg(feature = "auto-proxy")]
self.sender,
self.upstream_proxy,
self.honor_forced_direct,
)
.await
}
}

pub struct ClientProxyContext {
stream: TcpStream,
#[cfg(feature = "auto-proxy")]
Expand All @@ -144,6 +188,8 @@ pub struct ClientProxyContext {
upstream_proxy: Option<ExternalProxyTarget>,
honor_forced_direct: bool,
enable_udp: bool,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
process_matcher: Option<Arc<process_bypass::LocalPolicy>>,
}

async fn handle_proxy(
Expand Down Expand Up @@ -206,6 +252,24 @@ pub async fn handle_client(mut context: ClientProxyContext) -> Result<()> {
detail: "Read First Header Error",
})?;
let header = header_buf.as_slice();
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let policy = if let Some(matcher) = &context.process_matcher {
match (context.stream.peer_addr(), context.stream.local_addr()) {
(Ok(peer), Ok(local)) => matcher.matcher.match_tcp(peer, local).await,
Comment thread
acking-you marked this conversation as resolved.
Resolved
_ => None,
}
} else {
None
};
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let direct_interface = context.process_matcher.as_ref().and_then(|matcher| {
policy
.as_ref()
.is_some_and(tun2proxy::SessionPolicy::bypass)
.then(|| {
process_bypass::DirectRoute::new(matcher.virtual_dns.clone(), matcher.dns_fallback)
})
});
let header_context = HeaderContext { header };
let proxy_context = ProxyContext {
handshake_deadline: deadline,
Expand All @@ -216,24 +280,39 @@ pub async fn handle_client(mut context: ClientProxyContext) -> Result<()> {
upstream_proxy: context.upstream_proxy.as_ref(),
honor_forced_direct: context.honor_forced_direct,
enable_udp: context.enable_udp,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
direct_interface: direct_interface.as_ref(),
stream: context.stream,
};

if header.first() == Some(&5) {
handle_proxy(
proxy_context,
SocksProxierProvider::try_new(header_context)?,
permit,
)
.await
} else {
handle_proxy(
proxy_context,
HttpProxierProvider::try_new(header_context)?,
permit,
)
.await
let relay = async {
if header.first() == Some(&5) {
handle_proxy(
proxy_context,
SocksProxierProvider::try_new(header_context)?,
permit,
)
.await
} else {
handle_proxy(
proxy_context,
HttpProxierProvider::try_new(header_context)?,
permit,
)
.await
}
};
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if let Some(policy) = policy {
return tokio::select! {
result = relay => result,
_ = policy.wait_for_routing_change() => {
tracing::info!("local proxy connection closed after process policy changed");
Ok(())
}
};
}
relay.await
}

#[cfg(feature = "auto-proxy")]
Expand Down Expand Up @@ -293,6 +372,24 @@ pub async fn run_client_with_listener_runtime_config(
.unwrap_or(true);
let upstream_proxy = config.as_ref().and_then(|c| c.upstream_proxy.clone());
let tun_config = config.as_ref().and_then(|c| c.tun.clone());
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
let process_matcher = config
.as_ref()
.and_then(|c| c.process_bypass.clone())
.or_else(|| {
tun_config.as_ref().map(|c| ProcessBypassConfig {
processes: c.bypass.user_process_bypass(),
virtual_dns: c.virtual_dns_state.clone(),
dns_fallback: Some(c.dns_server),
})
})
.map(|config| {
Arc::new(process_bypass::LocalPolicy {
matcher: tun2proxy::LocalProcessMatcher::new(config.processes),
virtual_dns: config.virtual_dns,
dns_fallback: config.dns_fallback.unwrap_or(tun::DEFAULT_TUN_DNS_SERVER),
})
});
let force_proxy_controller = config.as_ref().and_then(|c| c.force_proxy.clone());
let enable_udp = config.as_ref().map(|c| c.client.enable_udp).unwrap_or(true);
let tun_active = tun_config.is_some();
Expand Down Expand Up @@ -407,6 +504,8 @@ pub async fn run_client_with_listener_runtime_config(
// See `proxy_core::config::FORCED_DIRECT_HOSTS`.
honor_forced_direct: !tun_owns_routes,
enable_udp,
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
process_matcher: process_matcher.clone(),
});
let wrapped_task = async move {
tokio::select! {
Expand Down
1 change: 1 addition & 0 deletions crates/proxy-client/src/client/packet_tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ impl PacketTunnelRuntime {
// enumeration or userspace interface binding is needed for direct
// decisions made by the existing auto-proxy implementation.
force_proxy: None,
process_bypass: None,
};
let (input, input_rx) = mpsc::channel(QUEUE_CAPACITY);
let (output_tx, output) = mpsc::channel(QUEUE_CAPACITY);
Expand Down
Loading
Loading