From b5907005403f83d608652284f224ceee20e1831a Mon Sep 17 00:00:00 2001 From: Chenx Dust Date: Sun, 11 Jan 2026 16:37:32 +0800 Subject: [PATCH] feat: support unix socket tunnel (for ios) (#1779) Co-authored-by: Page Chen --- easytier/src/connector/mod.rs | 7 + easytier/src/instance/listeners.rs | 5 + easytier/src/tunnel/mod.rs | 3 + easytier/src/tunnel/unix.rs | 216 +++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 easytier/src/tunnel/unix.rs diff --git a/easytier/src/connector/mod.rs b/easytier/src/connector/mod.rs index 5fe5e15e..2ded7406 100644 --- a/easytier/src/connector/mod.rs +++ b/easytier/src/connector/mod.rs @@ -7,6 +7,8 @@ use http_connector::HttpTunnelConnector; #[cfg(feature = "quic")] use crate::tunnel::quic::QUICTunnelConnector; +#[cfg(unix)] +use crate::tunnel::unix::UnixSocketTunnelConnector; #[cfg(feature = "wireguard")] use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector}; use crate::{ @@ -177,6 +179,11 @@ pub async fn create_connector_by_url( } Box::new(connector) } + #[cfg(unix)] + "unix" => { + let connector = UnixSocketTunnelConnector::new(url); + Box::new(connector) + } _ => { return Err(Error::InvalidUrl(url.into())); } diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index 93d090ed..0cdc3bf0 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -50,6 +50,11 @@ pub fn get_listener_by_url( Box::new(WSTunnelListener::new(l.clone())) } "faketcp" => Box::new(FakeTcpTunnelListener::new(l.clone())), + #[cfg(unix)] + "unix" => { + use crate::tunnel::unix::UnixSocketTunnelListener; + Box::new(UnixSocketTunnelListener::new(l.clone())) + } _ => { return Err(Error::InvalidUrl(l.to_string())); } diff --git a/easytier/src/tunnel/mod.rs b/easytier/src/tunnel/mod.rs index 2cd51830..f1efa9f1 100644 --- a/easytier/src/tunnel/mod.rs +++ b/easytier/src/tunnel/mod.rs @@ -45,6 +45,9 @@ pub mod websocket; #[cfg(any(feature = "quic", feature = "websocket"))] pub mod insecure_tls; +#[cfg(unix)] +pub mod unix; + #[derive(thiserror::Error, Debug)] pub enum TunnelError { #[error("io error")] diff --git a/easytier/src/tunnel/unix.rs b/easytier/src/tunnel/unix.rs new file mode 100644 index 00000000..a2f0e4ed --- /dev/null +++ b/easytier/src/tunnel/unix.rs @@ -0,0 +1,216 @@ +use std::path::Path; + +use async_trait::async_trait; +use tokio::net::{unix::SocketAddr, UnixListener, UnixStream}; + +use super::TunnelInfo; + +use super::{ + common::{FramedReader, FramedWriter, TunnelWrapper}, + IpVersion, Tunnel, TunnelError, TunnelListener, +}; + +const MAX_PACKET_SIZE: usize = 4096; + +fn url_from_unix_socket_addr(addr: SocketAddr) -> Option { + addr.as_pathname() + .and_then(|p| p.to_str()) + .and_then(|s| format!("unix://{}", s).parse().ok()) +} + +#[derive(Debug)] +pub struct UnixSocketTunnelListener { + addr: url::Url, + listener: Option, + unlink_on_drop: bool, +} + +impl UnixSocketTunnelListener { + pub fn new(addr: url::Url) -> Self { + UnixSocketTunnelListener { + addr, + listener: None, + unlink_on_drop: true, + } + } + + async fn do_accept(&self) -> Result, std::io::Error> { + let listener = self.listener.as_ref().unwrap(); + let (stream, _) = listener.accept().await?; + + let remote_addr = stream.peer_addr().ok().and_then(url_from_unix_socket_addr); + + let info = TunnelInfo { + tunnel_type: "unix".to_owned(), + local_addr: Some(self.local_url().into()), + remote_addr: remote_addr.map(Into::into), + }; + + let (r, w) = stream.into_split(); + Ok(Box::new(TunnelWrapper::new( + FramedReader::new(r, MAX_PACKET_SIZE), + FramedWriter::new(w), + Some(info), + ))) + } + + fn set_unlink_on_drop(&mut self, unlink: bool) { + self.unlink_on_drop = unlink; + } +} + +#[async_trait] +impl TunnelListener for UnixSocketTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + self.listener = None; + let path_str = self.addr.path(); + let path = Path::new(path_str); + + let listener = UnixListener::bind(path)?; + self.listener = Some(listener); + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + loop { + match self.do_accept().await { + Ok(ret) => return Ok(ret), + Err(e) => { + use std::io::ErrorKind::*; + if matches!( + e.kind(), + NotConnected | ConnectionAborted | ConnectionRefused | ConnectionReset + ) { + tracing::warn!(?e, "accept fail with retryable error: {:?}", e); + continue; + } + tracing::warn!(?e, "accept fail"); + return Err(e.into()); + } + } + } + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +#[derive(Debug)] +pub struct UnixSocketTunnelConnector { + addr: url::Url, +} + +impl UnixSocketTunnelConnector { + pub fn new(addr: url::Url) -> Self { + UnixSocketTunnelConnector { addr } + } +} + +#[async_trait] +impl super::TunnelConnector for UnixSocketTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let path_str = self.addr.path(); + let path = Path::new(path_str); + tracing::info!(url = ?self.addr, "connect unix socket start"); + let stream = UnixStream::connect(path).await?; + tracing::info!(url = ?self.addr, "connect unix socket succ"); + + let local_addr = stream.local_addr().ok().and_then(url_from_unix_socket_addr); + + let info = TunnelInfo { + tunnel_type: "unix".to_owned(), + local_addr: local_addr.map(Into::into), + remote_addr: Some(self.addr.clone().into()), + }; + + let (r, w) = stream.into_split(); + Ok(Box::new(TunnelWrapper::new( + FramedReader::new(r, MAX_PACKET_SIZE), + FramedWriter::new(w), + Some(info), + ))) + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_ip_version(&mut self, _ip_version: IpVersion) { + // IP version is not applicable to UNIX sockets + } +} + +impl Drop for UnixSocketTunnelListener { + fn drop(&mut self) { + if self.unlink_on_drop { + let _ = std::fs::remove_file(self.addr.path()); + } + } +} + +#[cfg(test)] +mod tests { + use crate::tunnel::common::tests::{_tunnel_bench, _tunnel_pingpong}; + + use super::*; + + #[tokio::test] + async fn unix_socket_pingpong() { + let listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test.sock".parse().unwrap()); + let connector = + UnixSocketTunnelConnector::new("unix:///tmp/easytier-test.sock".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn unix_socket_bench() { + let listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test-bench.sock".parse().unwrap()); + let connector = + UnixSocketTunnelConnector::new("unix:///tmp/easytier-test-bench.sock".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn unlink_on_drop() { + let listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test-exists.sock".parse().unwrap()); + let connector = UnixSocketTunnelConnector::new( + "unix:///tmp/easytier-test-exists.sock".parse().unwrap(), + ); + _tunnel_pingpong(listener, connector).await; + + let mut listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test-exists.sock".parse().unwrap()); + listener.set_unlink_on_drop(false); + let connector = UnixSocketTunnelConnector::new( + "unix:///tmp/easytier-test-exists.sock".parse().unwrap(), + ); + _tunnel_pingpong(listener, connector).await; + + let mut listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test-exists.sock".parse().unwrap()); + let result = listener.listen().await; + assert!( + matches!(result, Err(TunnelError::IOError(err)) if err.kind() == std::io::ErrorKind::AddrInUse) + ) + } + + #[tokio::test] + async fn bind_file_exists() { + use std::fs; + + let path = "/tmp/easytier-test-exists.sock"; + fs::File::create(path).unwrap(); + let mut listener = + UnixSocketTunnelListener::new("unix:///tmp/easytier-test-exists.sock".parse().unwrap()); + let result = listener.listen().await; + + fs::remove_file(path).unwrap(); + assert!( + matches!(result, Err(TunnelError::IOError(err)) if err.kind() == std::io::ErrorKind::AddrInUse) + ) + } +}