mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-06 17:59:11 +00:00
feat: support unix socket tunnel (for ios) (#1779)
Co-authored-by: Page Chen <pagechen04@gmail.com>
This commit is contained in:
@@ -7,6 +7,8 @@ use http_connector::HttpTunnelConnector;
|
|||||||
|
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
use crate::tunnel::quic::QUICTunnelConnector;
|
use crate::tunnel::quic::QUICTunnelConnector;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::tunnel::unix::UnixSocketTunnelConnector;
|
||||||
#[cfg(feature = "wireguard")]
|
#[cfg(feature = "wireguard")]
|
||||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
|
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -177,6 +179,11 @@ pub async fn create_connector_by_url(
|
|||||||
}
|
}
|
||||||
Box::new(connector)
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
"unix" => {
|
||||||
|
let connector = UnixSocketTunnelConnector::new(url);
|
||||||
|
Box::new(connector)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::InvalidUrl(url.into()));
|
return Err(Error::InvalidUrl(url.into()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ pub fn get_listener_by_url(
|
|||||||
Box::new(WSTunnelListener::new(l.clone()))
|
Box::new(WSTunnelListener::new(l.clone()))
|
||||||
}
|
}
|
||||||
"faketcp" => Box::new(FakeTcpTunnelListener::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()));
|
return Err(Error::InvalidUrl(l.to_string()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ pub mod websocket;
|
|||||||
#[cfg(any(feature = "quic", feature = "websocket"))]
|
#[cfg(any(feature = "quic", feature = "websocket"))]
|
||||||
pub mod insecure_tls;
|
pub mod insecure_tls;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub mod unix;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum TunnelError {
|
pub enum TunnelError {
|
||||||
#[error("io error")]
|
#[error("io error")]
|
||||||
|
|||||||
@@ -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<url::Url> {
|
||||||
|
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<UnixListener>,
|
||||||
|
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<Box<dyn Tunnel>, 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<Box<dyn Tunnel>, 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<Box<dyn Tunnel>, 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user