feat: Add data plane support to FFI (#2287)

1. Overview

This PR adds data plane APIs to easytier-ffi:

TCP Outbound:

- data_plane_tcp_connect
- data_plane_tcp_read
- data_plane_tcp_write
- data_plane_tcp_close

TCP Listener:

- data_plane_tcp_bind
- data_plane_tcp_accept
- data_plane_tcp_listener_close

UDP:

- data_plane_udp_bind
- data_plane_udp_send_to
- data_plane_udp_recv_from
- data_plane_udp_close

2. Key Changes

The main changes are focused on:

- easytier-contrib/easytier-ffi/src/lib.rs: Added FFI interfaces;
  made ERROR_MSG thread-safe.

- easytier/src/gateway/socks5.rs: Bridges the data plane to the
  existing Socks5 server logic.
  - Added EasyTierUdpSocket, mainly wrapping ref-counting and
    critical object (e.g., Socks5EntrySet) hold & drop logic,
    and exposing common fields (e.g., local_addr).
  - Extended Socks5Server functionality to expose TCP and UDP
    socket creation interfaces for FFI calls.

- Other files: Mostly pass-through logic.

- Added a relatively large Go usage example.
This commit is contained in:
w568w
2026-06-04 17:17:41 +08:00
committed by GitHub
parent e3ca7ffa54
commit e0745f4bab
14 changed files with 2336 additions and 21 deletions
+1
View File
@@ -391,6 +391,7 @@ websocket = [
]
smoltcp = ["dep:smoltcp"]
socks5 = ["smoltcp"]
ffi-dataplane = ["socks5"]
jemalloc = ["dep:jemallocator", "dep:jemalloc-sys"]
jemalloc-prof = [
"jemalloc",
+57 -3
View File
@@ -52,6 +52,12 @@ use crate::{
peers::{PeerPacketFilter, peer_manager::PeerManager},
};
#[cfg(feature = "ffi-dataplane")]
mod dataplane;
#[cfg(feature = "ffi-dataplane")]
pub use dataplane::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
enum SocksUdpSocket {
UdpSocket(Arc<tokio::net::UdpSocket>),
SmolUdpSocket(super::tokio_smoltcp::UdpSocket),
@@ -136,11 +142,17 @@ impl AsyncWrite for SocksTcpStream {
enum Socks5EntryData {
Tcp(TcpListener), // hold a binded socket to hold the tcp port
#[cfg(feature = "ffi-dataplane")]
// a data-plane routing entry that owns no resource. the entry_type in the
// key distinguishes a listen route from an actively outbound route.
DataPlaneRoute,
Udp((Arc<SocksUdpSocket>, UdpClientKey)), // hold the socket to send data to dst
}
const UDP_ENTRY: u8 = 1;
const TCP_ENTRY: u8 = 2;
#[cfg(feature = "ffi-dataplane")]
const TCP_LISTEN_ENTRY: u8 = 3;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Socks5Entry {
@@ -477,6 +489,11 @@ pub struct Socks5Server {
kcp_endpoint: Mutex<Option<Weak<KcpEndpoint>>>,
socks5_enabled: Arc<AtomicBool>,
#[cfg(feature = "ffi-dataplane")]
data_plane_refs: Arc<AtomicUsize>,
// Tracks whether the smoltcp `net` is ready for data-plane callers.
#[cfg(feature = "ffi-dataplane")]
data_plane_net_ready: tokio::sync::watch::Sender<bool>,
cancel_tokens: Arc<DashMap<PortForwardConfig, DropGuard>>,
port_forward_list_change_notifier: Arc<Notify>,
entry_count: Arc<AtomicUsize>,
@@ -508,14 +525,27 @@ impl PeerPacketFilter for Socks5Server {
let Some(tcp_packet) = TcpPacket::new(ipv4.payload()) else {
return Some(packet);
};
Socks5Entry {
let entry = Socks5Entry {
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
src: SocketAddr::new(
ipv4.get_destination().into(),
tcp_packet.get_destination(),
),
entry_type: TCP_ENTRY,
}
};
#[cfg(feature = "ffi-dataplane")]
let entry = if self.entries.contains_key(&entry) {
// Case 1: it is an established connection that has an exactly matched inbound.
entry
} else {
// Case 2: it could be a new TCP SYN packet that has not been accepted.
Socks5Entry {
src: entry.src,
dst: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
entry_type: TCP_LISTEN_ENTRY,
}
};
entry
}
IpNextHeaderProtocols::Udp => {
@@ -591,6 +621,10 @@ impl Socks5Server {
kcp_endpoint: Mutex::new(None),
socks5_enabled: Arc::new(AtomicBool::new(false)),
#[cfg(feature = "ffi-dataplane")]
data_plane_refs: Arc::new(AtomicUsize::new(0)),
#[cfg(feature = "ffi-dataplane")]
data_plane_net_ready: tokio::sync::watch::channel(false).0,
cancel_tokens: Arc::new(DashMap::new()),
port_forward_list_change_notifier: Arc::new(Notify::new()),
entry_count: Arc::new(AtomicUsize::new(0)),
@@ -608,11 +642,25 @@ impl Socks5Server {
let cancel_tokens = self.cancel_tokens.clone();
let port_forward_list_change_notifier = self.port_forward_list_change_notifier.clone();
let socks5_enabled = self.socks5_enabled.clone();
#[cfg(feature = "ffi-dataplane")]
let data_plane_refs = self.data_plane_refs.clone();
#[cfg(feature = "ffi-dataplane")]
let data_plane_net_ready = self.data_plane_net_ready.clone();
self.tasks.lock().unwrap().spawn(async move {
let mut prev_ipv4 = None;
loop {
if cancel_tokens.is_empty() && !socks5_enabled.load(Ordering::Relaxed) {
#[cfg(feature = "ffi-dataplane")]
let data_plane_active = data_plane_refs.load(Ordering::Relaxed) > 0;
#[cfg(not(feature = "ffi-dataplane"))]
let data_plane_active = false;
if cancel_tokens.is_empty()
&& !socks5_enabled.load(Ordering::Relaxed)
&& !data_plane_active
{
let _ = net.lock().await.take();
#[cfg(feature = "ffi-dataplane")]
let _ = data_plane_net_ready.send_replace(false);
port_forward_list_change_notifier.notified().await;
continue;
}
@@ -637,8 +685,14 @@ impl Socks5Server {
packet_recv.clone(),
entries.clone(),
));
// Wake any data-plane callers waiting in
// `wait_data_plane_net` for the smoltcp net to appear.
#[cfg(feature = "ffi-dataplane")]
let _ = data_plane_net_ready.send_replace(true);
} else {
let _ = net.lock().await.take();
#[cfg(feature = "ffi-dataplane")]
let _ = data_plane_net_ready.send_replace(false);
}
}
+535
View File
@@ -0,0 +1,535 @@
//! Data-plane access built on top of the `Socks5Server` smoltcp stack.
//!
//! This module exposes TCP streams and UDP sockets (mainly for FFI callers that
//! send traffic through EasyTier without creating OS-level proxy listeners).
//!
//! Typical usage:
//!
//! ```ignore
//! let instance = Instance::new(cfg);
//! instance.run().await?;
//! let socks5_server = instance.get_socks5_server();
//!
//! let socket = socks5_server.data_plane_udp_bind(local_port, timeout).await?;
//! socket.send_to(buf, peer_addr).await?;
//! ```
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
pin::Pin,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
task::{Context, Poll},
time::{Duration, Instant},
};
use anyhow::Context as _;
use dashmap::mapref::entry::Entry;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::{common::error::Error, gateway::fast_socks5::server::AsyncTcpConnector};
use super::{
Socks5AutoConnector, Socks5Entry, Socks5EntryData, Socks5EntrySet, Socks5Server,
SocksTcpStream, SocksUdpSocket, TCP_ENTRY, TCP_LISTEN_ENTRY, UDP_ENTRY, UdpClientKey,
};
use crate::gateway::tokio_smoltcp::{Net, TcpListener};
struct DataPlaneRef {
refs: Arc<AtomicUsize>,
notifier: Arc<tokio::sync::Notify>,
}
/// A route-table entry whose lifetime is tied to this value: constructing it
/// reserves the route and bumps the active-entry count, dropping it removes the
/// route and drops the count back.
struct OwnedRouteEntry {
entries: Socks5EntrySet,
entry_count: Arc<AtomicUsize>,
entry: Socks5Entry,
}
impl OwnedRouteEntry {
/// Inserts the route, replacing any existing entry for the same key.
fn register(
entries: Socks5EntrySet,
entry_count: Arc<AtomicUsize>,
entry: Socks5Entry,
) -> Self {
if entries
.insert(entry.clone(), Socks5EntryData::DataPlaneRoute)
.is_none()
{
entry_count.fetch_add(1, Ordering::Relaxed);
}
Self {
entries,
entry_count,
entry,
}
}
/// Inserts the route only if the key is free, returning `None` on conflict.
fn try_register(
entries: Socks5EntrySet,
entry_count: Arc<AtomicUsize>,
entry: Socks5Entry,
) -> Option<Self> {
match entries.entry(entry.clone()) {
Entry::Occupied(_) => return None,
Entry::Vacant(vacant) => {
vacant.insert(Socks5EntryData::DataPlaneRoute);
entry_count.fetch_add(1, Ordering::Relaxed);
}
}
Some(Self {
entries,
entry_count,
entry,
})
}
}
impl Drop for OwnedRouteEntry {
fn drop(&mut self) {
if self.entries.remove(&self.entry).is_some() {
self.entry_count.fetch_sub(1, Ordering::Relaxed);
}
}
}
/// Tracks how an established data-plane TCP stream keeps its inbound route alive.
///
/// The two variants capture the intrinsic asymmetry between the connect and
/// accept paths. An outbound stream reserved a source port through the
/// [`Socks5AutoConnector`], which owns the matching route entry and clears it on
/// drop. An accepted stream instead inherits its port and peer from the
/// listener, so it carries merely an [`OwnedRouteEntry`].
enum DataPlaneTcpStreamRoute {
Outbound(Socks5AutoConnector),
Accepted(OwnedRouteEntry),
}
/// A TCP stream created by the data plane API.
/// Can be either an actively requested outbound connection or an outbound request accepted from a TCP listener.
pub struct DataPlaneTcpStream {
stream: SocksTcpStream,
local_addr: SocketAddr,
_data_plane_ref: DataPlaneRef,
_route: DataPlaneTcpStreamRoute,
}
/// A TCP listener created by the data plane API.
/// It accepts inbound connections and produces [`DataPlaneTcpStream`]s.
pub struct DataPlaneTcpListener {
listener: TcpListener,
local_addr: SocketAddr,
entries: Socks5EntrySet,
entry_count: Arc<AtomicUsize>,
_listen_route: OwnedRouteEntry,
_data_plane_ref: DataPlaneRef,
}
pub struct DataPlaneUdpSocket {
socket: Arc<SocksUdpSocket>,
entries: Socks5EntrySet,
entry_count: Arc<AtomicUsize>,
local_addr: SocketAddr,
_data_plane_ref: DataPlaneRef,
}
impl Drop for DataPlaneRef {
fn drop(&mut self) {
if self.refs.fetch_sub(1, Ordering::Relaxed) == 1 {
self.notifier.notify_one();
}
}
}
impl DataPlaneTcpStream {
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
}
impl DataPlaneTcpListener {
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub async fn accept(&mut self) -> Result<(DataPlaneTcpStream, SocketAddr), std::io::Error> {
let (stream, peer_addr) = self.listener.accept().await?;
let local_addr = stream.local_addr()?;
let route = OwnedRouteEntry::register(
self.entries.clone(),
self.entry_count.clone(),
Socks5Entry {
src: local_addr,
dst: peer_addr,
entry_type: TCP_ENTRY,
},
);
let accepted = DataPlaneTcpStream {
stream: SocksTcpStream::SmolTcp(stream),
local_addr,
_data_plane_ref: self._data_plane_ref.clone(),
_route: DataPlaneTcpStreamRoute::Accepted(route),
};
Ok((accepted, peer_addr))
}
}
impl AsyncRead for DataPlaneTcpStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
}
}
impl AsyncWrite for DataPlaneTcpStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
Pin::new(&mut self.get_mut().stream).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.get_mut().stream).poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.get_mut().stream).poll_shutdown(cx)
}
}
impl DataPlaneUdpSocket {
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> Result<usize, std::io::Error> {
let key = Socks5Entry {
src: self.local_addr,
dst: addr,
entry_type: UDP_ENTRY,
};
if let Entry::Vacant(entry) = self.entries.entry(key) {
entry.insert(Socks5EntryData::Udp((
self.socket.clone(),
UdpClientKey {
client_addr: self.local_addr,
dst_addr: addr,
},
)));
self.entry_count.fetch_add(1, Ordering::Relaxed);
}
self.socket.send_to(buf, addr).await
}
pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr), std::io::Error> {
self.socket.recv_from(buf).await
}
}
impl Drop for DataPlaneUdpSocket {
fn drop(&mut self) {
self.entries.retain(|_, data| match data {
Socks5EntryData::Udp((socket, _)) if Arc::ptr_eq(socket, &self.socket) => {
self.entry_count.fetch_sub(1, Ordering::Relaxed);
false
}
_ => true,
});
}
}
impl Clone for DataPlaneRef {
fn clone(&self) -> Self {
self.refs.fetch_add(1, Ordering::Relaxed);
Self {
refs: self.refs.clone(),
notifier: self.notifier.clone(),
}
}
}
impl Socks5Server {
fn acquire_data_plane_ref(&self) -> DataPlaneRef {
self.data_plane_refs.fetch_add(1, Ordering::Relaxed);
self.port_forward_list_change_notifier.notify_one();
DataPlaneRef {
refs: self.data_plane_refs.clone(),
notifier: self.port_forward_list_change_notifier.clone(),
}
}
async fn wait_data_plane_net(
&self,
deadline: Instant,
) -> Result<(cidr::Ipv4Inet, Arc<Net>), Error> {
let mut ready = self.data_plane_net_ready.subscribe();
loop {
if let Some(net) = self
.net
.lock()
.await
.as_ref()
.map(|net| (net.ipv4_addr, net.smoltcp_net.clone()))
{
return Ok(net);
}
let now = Instant::now();
if now >= deadline {
return Err(anyhow::anyhow!("data plane net is not ready").into());
}
let _ = tokio::time::timeout(deadline - now, ready.wait_for(|ready| *ready)).await;
}
}
pub async fn data_plane_tcp_connect(
&self,
dst_addr: SocketAddr,
timeout: Duration,
) -> Result<DataPlaneTcpStream, Error> {
let data_plane_ref = self.acquire_data_plane_ref();
let deadline = Instant::now() + timeout;
let (ipv4_addr, smoltcp_net) = self.wait_data_plane_net(deadline).await?;
// FIXME: This is the data-plane source address reserved for route
// matching. `Socks5AutoConnector` may fall back to direct TCP for
// non-virtual destinations, so this is not always the OS socket's
// local address.
let local_port = smoltcp_net.get_port();
let local_addr = SocketAddr::new(IpAddr::V4(ipv4_addr.address()), local_port);
let connector = Socks5AutoConnector {
#[cfg(feature = "kcp")]
kcp_endpoint: self.kcp_endpoint.lock().await.clone(),
peer_mgr: self.peer_manager.clone(),
entries: self.entries.clone(),
smoltcp_net: Some(smoltcp_net),
src_addr: local_addr,
entry_count: self.entry_count.clone(),
inner_connector: parking_lot::Mutex::new(None),
};
let remaining = deadline.saturating_duration_since(Instant::now());
let inner_timeout_s = remaining.as_secs().saturating_add(1);
let stream =
tokio::time::timeout(remaining, connector.tcp_connect(dst_addr, inner_timeout_s))
.await
.with_context(|| "data plane tcp connect timeout")?
.map_err(anyhow::Error::from)?;
Ok(DataPlaneTcpStream {
stream,
local_addr,
_data_plane_ref: data_plane_ref,
_route: DataPlaneTcpStreamRoute::Outbound(connector),
})
}
pub async fn data_plane_tcp_bind(
&self,
local_port: u16,
timeout: Duration,
) -> Result<DataPlaneTcpListener, Error> {
let data_plane_ref = self.acquire_data_plane_ref();
let deadline = Instant::now() + timeout;
let (ipv4_addr, smoltcp_net) = self.wait_data_plane_net(deadline).await?;
let bind_addr = SocketAddr::new(IpAddr::V4(ipv4_addr.address()), local_port);
let listener = smoltcp_net.tcp_bind(bind_addr).await?;
let local_addr = listener.local_addr()?;
let listen_route = OwnedRouteEntry::try_register(
self.entries.clone(),
self.entry_count.clone(),
Socks5Entry {
src: local_addr,
dst: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
entry_type: TCP_LISTEN_ENTRY,
},
)
.ok_or_else(|| anyhow::anyhow!("data plane tcp listener already exists"))?;
Ok(DataPlaneTcpListener {
listener,
local_addr,
entries: self.entries.clone(),
entry_count: self.entry_count.clone(),
_listen_route: listen_route,
_data_plane_ref: data_plane_ref,
})
}
pub async fn data_plane_udp_bind(
&self,
local_port: u16,
timeout: Duration,
) -> Result<DataPlaneUdpSocket, Error> {
let data_plane_ref = self.acquire_data_plane_ref();
let deadline = Instant::now() + timeout;
let (ipv4_addr, smoltcp_net) = self.wait_data_plane_net(deadline).await?;
let bind_addr = SocketAddr::new(IpAddr::V4(ipv4_addr.address()), local_port);
let smol = smoltcp_net.udp_bind(bind_addr).await?;
let local_addr = smol.local_addr()?;
let socket = Arc::new(SocksUdpSocket::SmolUdpSocket(smol));
Ok(DataPlaneUdpSocket {
socket,
entries: self.entries.clone(),
entry_count: self.entry_count.clone(),
local_addr,
_data_plane_ref: data_plane_ref,
})
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::Socks5Server;
use crate::peers::peer_manager::PeerManager;
use crate::peers::tests::{connect_peer_manager, create_mock_peer_manager};
use crate::tunnel::common::tests::wait_for_condition;
/// A peer and its data-plane server. `Socks5Server` only holds a `Weak`
/// reference to the `PeerManager`, so the manager must be kept alive by the
/// test for the server's smoltcp <-> peer routing to work.
struct Endpoint {
_peer: std::sync::Arc<PeerManager>,
server: std::sync::Arc<Socks5Server>,
ip: cidr::Ipv4Inet,
}
/// Brings up two peers connected by a ring tunnel, each with a virtual IPv4
/// and a running `Socks5Server`, and waits until the route to `b`'s IPv4 is
/// visible from `a`. `run(None)` leaves the kcp endpoint unset, so the
/// connect path goes through smoltcp, matching the listener side under test.
async fn setup_pair() -> (Endpoint, Endpoint) {
let a = create_mock_peer_manager().await;
let b = create_mock_peer_manager().await;
connect_peer_manager(a.clone(), b.clone()).await;
let a_ip: cidr::Ipv4Inet = "10.126.126.1/24".parse().unwrap();
let b_ip: cidr::Ipv4Inet = "10.126.126.2/24".parse().unwrap();
a.get_global_ctx().set_ipv4(Some(a_ip));
b.get_global_ctx().set_ipv4(Some(b_ip));
let server_a = Socks5Server::new(a.get_global_ctx(), a.clone(), None);
let server_b = Socks5Server::new(b.get_global_ctx(), b.clone(), None);
server_a.run(None).await.unwrap();
server_b.run(None).await.unwrap();
wait_for_condition(
|| async {
a.get_route()
.get_peer_id_by_ipv4(&b_ip.address())
.await
.is_some()
},
Duration::from_secs(10),
)
.await;
(
Endpoint {
_peer: a,
server: server_a,
ip: a_ip,
},
Endpoint {
_peer: b,
server: server_b,
ip: b_ip,
},
)
}
#[tokio::test]
async fn data_plane_tcp_pingpong() {
let (ep_a, ep_b) = setup_pair().await;
let (server_a, server_b, b_ip) = (ep_a.server, ep_b.server, ep_b.ip);
let timeout = Duration::from_secs(10);
let mut listener = server_b.data_plane_tcp_bind(0, timeout).await.unwrap();
let listen_addr =
std::net::SocketAddr::new(b_ip.address().into(), listener.local_addr().port());
let accept = tokio::spawn(async move {
let (mut stream, _peer) = listener.accept().await.unwrap();
let mut buf = [0u8; 4];
stream.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
stream.write_all(b"pong").await.unwrap();
stream.flush().await.unwrap();
// Hold the listener and stream until the client has read the reply.
tokio::time::sleep(Duration::from_secs(1)).await;
});
let mut client = server_a
.data_plane_tcp_connect(listen_addr, timeout)
.await
.unwrap();
client.write_all(b"ping").await.unwrap();
client.flush().await.unwrap();
let mut buf = [0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"pong");
accept.await.unwrap();
}
#[tokio::test]
async fn data_plane_udp_pingpong() {
let (ep_a, ep_b) = setup_pair().await;
let (server_a, a_ip, server_b, b_ip) = (ep_a.server, ep_a.ip, ep_b.server, ep_b.ip);
let timeout = Duration::from_secs(10);
let sock_a = server_a.data_plane_udp_bind(0, timeout).await.unwrap();
let sock_b = server_b.data_plane_udp_bind(0, timeout).await.unwrap();
let addr_a = std::net::SocketAddr::new(a_ip.address().into(), sock_a.local_addr().port());
let addr_b = std::net::SocketAddr::new(b_ip.address().into(), sock_b.local_addr().port());
// UDP data-plane routes are connected-style: a socket only accepts
// inbound datagrams from a peer it has already sent to, because the
// route entry is registered by `send_to`. Prime b's route toward a so
// the upcoming ping is routed instead of dropped at b's packet filter.
// This datagram is dropped at a (a has no route yet) and is not awaited.
sock_b.send_to(b"warmup", addr_a).await.unwrap();
sock_a.send_to(b"ping", addr_b).await.unwrap();
let mut buf = [0u8; 16];
let (n, from) = tokio::time::timeout(timeout, sock_b.recv_from(&mut buf))
.await
.expect("recv ping timed out")
.unwrap();
assert_eq!(&buf[..n], b"ping");
assert_eq!(from, addr_a);
sock_b.send_to(b"pong", addr_a).await.unwrap();
// a may also receive the stray warmup datagram (it arrives once a has
// registered its route by sending the ping above), so skip anything
// that is not the reply.
loop {
let (n, from) = tokio::time::timeout(timeout, sock_a.recv_from(&mut buf))
.await
.expect("recv pong timed out")
.unwrap();
if &buf[..n] == b"pong" {
assert_eq!(from, addr_b);
break;
}
}
}
}
+5
View File
@@ -1143,6 +1143,11 @@ impl Instance {
self.peer_manager.clone()
}
#[cfg(feature = "ffi-dataplane")]
pub fn get_socks5_server(&self) -> Arc<Socks5Server> {
self.socks5_server.clone()
}
pub async fn close_peer_conn(
&mut self,
peer_id: PeerId,
+55
View File
@@ -1,3 +1,5 @@
#[cfg(feature = "ffi-dataplane")]
use crate::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
use dashmap::DashMap;
use std::fmt::{Display, Formatter};
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
@@ -171,6 +173,59 @@ impl NetworkInstanceManager {
tokio::runtime::Runtime::new()?.block_on(self.collect_network_infos())
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_tcp_connect(
&self,
instance_id: &uuid::Uuid,
dst_addr: std::net::SocketAddr,
timeout: std::time::Duration,
) -> Result<DataPlaneTcpStream, anyhow::Error> {
let instance = self
.instance_map
.get(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
instance.data_plane_tcp_connect(dst_addr, timeout).await
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_tcp_bind(
&self,
instance_id: &uuid::Uuid,
local_port: u16,
timeout: std::time::Duration,
) -> Result<DataPlaneTcpListener, anyhow::Error> {
let instance = self
.instance_map
.get(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
instance.data_plane_tcp_bind(local_port, timeout).await
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_udp_bind(
&self,
instance_id: &uuid::Uuid,
local_port: u16,
timeout: std::time::Duration,
) -> Result<DataPlaneUdpSocket, anyhow::Error> {
let instance = self
.instance_map
.get(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?;
instance.data_plane_udp_bind(local_port, timeout).await
}
#[cfg(feature = "ffi-dataplane")]
pub fn data_plane_wait_runtime_handle(
&self,
instance_id: &uuid::Uuid,
timeout: std::time::Duration,
) -> Option<tokio::runtime::Handle> {
self.instance_map
.get(instance_id)
.and_then(|inst| inst.wait_runtime_handle(timeout))
}
pub async fn get_network_info(
&self,
instance_id: &uuid::Uuid,
+132
View File
@@ -2,6 +2,10 @@ use crate::common::config::{
ConfigFileControl, ConfigSource, PortForwardConfig, parse_mapped_listener_urls,
process_secure_mode_cfg,
};
#[cfg(feature = "ffi-dataplane")]
use crate::gateway::socks5::Socks5Server;
#[cfg(feature = "ffi-dataplane")]
pub use crate::gateway::socks5::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
use crate::proto::api::{self, manage};
use crate::proto::rpc_types::controller::BaseController;
use crate::rpc_service::InstanceRpcService;
@@ -45,6 +49,13 @@ struct EasyTierData {
tun_fd: (mpsc::Sender<TunFd>, Mutex<Option<mpsc::Receiver<TunFd>>>),
event_subscriber: RwLock<broadcast::Sender<GlobalCtxEvent>>,
instance_stop_notifier: Arc<tokio::sync::Notify>,
#[cfg(feature = "ffi-dataplane")]
data_plane: tokio::sync::watch::Sender<Option<Arc<Socks5Server>>>,
#[cfg(feature = "ffi-dataplane")]
runtime_handle: (
parking_lot::Mutex<Option<tokio::runtime::Handle>>,
parking_lot::Condvar,
),
}
impl Default for EasyTierData {
@@ -56,6 +67,10 @@ impl Default for EasyTierData {
events: RwLock::new(VecDeque::new()),
tun_fd: (sender, Mutex::new(Some(receiver))),
instance_stop_notifier: Arc::new(tokio::sync::Notify::new()),
#[cfg(feature = "ffi-dataplane")]
data_plane: tokio::sync::watch::channel(None).0,
#[cfg(feature = "ffi-dataplane")]
runtime_handle: (parking_lot::Mutex::new(None), parking_lot::Condvar::new()),
}
}
}
@@ -160,6 +175,10 @@ impl EasyTierLauncher {
instance.run().await?;
#[cfg(feature = "ffi-dataplane")]
data.data_plane
.send_replace(Some(instance.get_socks5_server()));
api_service
.write()
.unwrap()
@@ -214,6 +233,13 @@ impl EasyTierLauncher {
}
.unwrap();
#[cfg(feature = "ffi-dataplane")]
{
let (lock, cvar) = &data.runtime_handle;
*lock.lock() = Some(rt.handle().clone());
cvar.notify_all();
}
let stop_notifier = Arc::new(tokio::sync::Notify::new());
let stop_notifier_clone = stop_notifier.clone();
@@ -263,6 +289,43 @@ impl EasyTierLauncher {
}
}
}
#[cfg(feature = "ffi-dataplane")]
pub fn get_data_plane(&self) -> Option<Arc<Socks5Server>> {
self.data.data_plane.borrow().clone()
}
/// Waits up to `deadline` for the data-plane server to be published.
#[cfg(feature = "ffi-dataplane")]
pub async fn wait_data_plane(
&self,
deadline: tokio::time::Instant,
) -> Option<Arc<Socks5Server>> {
let mut rx = self.data.data_plane.subscribe();
loop {
if let Some(server) = rx.borrow_and_update().clone() {
return Some(server);
}
if tokio::time::timeout_at(deadline, rx.changed())
.await
.is_err()
{
return None;
}
}
}
/// Blocks up to `timeout` for the runtime handle to be published.
#[cfg(feature = "ffi-dataplane")]
pub fn wait_runtime_handle(
&self,
timeout: std::time::Duration,
) -> Option<tokio::runtime::Handle> {
let (lock, cvar) = &self.data.runtime_handle;
let mut guard = lock.lock();
cvar.wait_while_for(&mut guard, |h| h.is_none(), timeout);
guard.clone()
}
}
impl Default for EasyTierLauncher {
@@ -454,6 +517,75 @@ impl NetworkInstance {
.as_ref()
.and_then(|launcher| launcher.get_api_service())
}
/// Waits up to `timeout` for the data-plane server to come up, returning it
/// together with the deadline so the caller can spend the remaining budget
/// on the actual operation.
#[cfg(feature = "ffi-dataplane")]
async fn wait_data_plane(
&self,
timeout: std::time::Duration,
) -> anyhow::Result<(Arc<Socks5Server>, tokio::time::Instant)> {
let deadline = tokio::time::Instant::now() + timeout;
let launcher = self
.launcher
.as_ref()
.ok_or_else(|| anyhow::anyhow!("data plane is not ready"))?;
let server = launcher
.wait_data_plane(deadline)
.await
.ok_or_else(|| anyhow::anyhow!("data plane is not ready"))?;
Ok((server, deadline))
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_tcp_connect(
&self,
dst_addr: SocketAddr,
timeout: std::time::Duration,
) -> anyhow::Result<DataPlaneTcpStream> {
let (server, deadline) = self.wait_data_plane(timeout).await?;
server
.data_plane_tcp_connect(dst_addr, deadline - tokio::time::Instant::now())
.await
.map_err(Into::into)
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_tcp_bind(
&self,
local_port: u16,
timeout: std::time::Duration,
) -> anyhow::Result<DataPlaneTcpListener> {
let (server, deadline) = self.wait_data_plane(timeout).await?;
server
.data_plane_tcp_bind(local_port, deadline - tokio::time::Instant::now())
.await
.map_err(Into::into)
}
#[cfg(feature = "ffi-dataplane")]
pub async fn data_plane_udp_bind(
&self,
local_port: u16,
timeout: std::time::Duration,
) -> anyhow::Result<DataPlaneUdpSocket> {
let (server, deadline) = self.wait_data_plane(timeout).await?;
server
.data_plane_udp_bind(local_port, deadline - tokio::time::Instant::now())
.await
.map_err(Into::into)
}
#[cfg(feature = "ffi-dataplane")]
pub fn wait_runtime_handle(
&self,
timeout: std::time::Duration,
) -> Option<tokio::runtime::Handle> {
self.launcher
.as_ref()
.and_then(|launcher| launcher.wait_runtime_handle(timeout))
}
}
pub fn add_proxy_network_to_config(