mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-04 01:55:41 +00:00
* feat(mobile): add embedded iOS runtime API Add a thin panic-safe C ABI crate for embedding no-TUN instances on iOS. Expose lifecycle, status, JSON-RPC, string ownership, and error handling. Build device and simulator XCFramework static libraries on macOS. Add exact named-instance deletion to the iOS and Android wrappers. Cover wrapper lifecycle and the port-forward patch flow on host targets. * fix(gateway): recover TCP port-forward listeners Release an unusable TCP port-forward listener after an accept failure. Retry binding until the forward is cancelled. Keep the old listener released while rebinding so mobile sockets can recover. Expose opt-in iOS diagnostics for listener and connection events. Trace configuration removal and adapter shutdown. Add tests for recovery, release-before-rebind, and cancellation. * feat(web): persist incremental managed config patches Add a revision-CAS PATCH contract for managed configs while keeping the existing Full PUT path for compatibility and recovery. Apply Full and Patch mutations with their revision in one SQLite transaction. Reject ownership conflicts and invalidate revisions on alternate web-owned writes. Document limits, failure semantics, rollout order, and verification. Cover delta updates, conflicts, idempotency, and transaction rollback. * feat(web): apply managed config patches to live sessions Carry Patch fences and touched instance IDs into live sessions. Reconcile only those instances when the applied revision matches the Patch base. Fall back to Full reconciliation for gaps and restarts. Invalidate the applied revision around every direct runtime mutation. Fence revision advancement with the runtime cache epoch so stale reconcile rounds cannot overwrite a newer invalidation. Require deletion responses to confirm each requested instance before advancing the revision. Raise the managed PUT and PATCH body limit to 32 MiB and return typed conflicts for publisher recovery. * fix(core): retry transient accepted TCP errors Keep TCP tunnel listeners alive when an accepted socket fails during upgrade with a retryable connection-state error. Share the retryable I/O classifier with the socket listener. Cover a rejected connection followed by success and propagation of permanent errors. * feat(core): add internal Peer Relay edge projection Derive the local advertised OSPF row from physical adjacency and transport-authenticated credential relay coverage. Keep full local adjacency only in the temporary SPF snapshot so direct destinations retain a fallback route. Leave Peer Relay disabled at the public configuration seam. A follow-up change can expose the preference without coupling route projection to credential reauthorization. feat(config): expose Peer Relay routing preference Add prefer_peer_relay to public protobuf, TOML, management patch, and hosted runtime surfaces. Read the preference from live peer context so runtime config updates take effect. Refresh authenticated peer metadata when the option is enabled. Cover dynamic enable and disable in a five-node, dual-admin credential topology, including forwarded relay coverage and local fallback.
700 lines
21 KiB
Rust
700 lines
21 KiB
Rust
use std::{fmt, io, net::SocketAddr, sync::Arc};
|
|
|
|
use async_trait::async_trait;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::io::{AsyncRead, AsyncWrite};
|
|
|
|
use crate::socket::{IpVersion, SocketContext, SocketListener};
|
|
|
|
pub type VirtualTcpReadHalf = Box<dyn AsyncRead + Unpin + Send + 'static>;
|
|
pub type VirtualTcpWriteHalf = Box<dyn AsyncWrite + Unpin + Send + 'static>;
|
|
pub type VirtualTcpSplit = (VirtualTcpReadHalf, VirtualTcpWriteHalf);
|
|
|
|
/// A core-visible TCP stream endpoint.
|
|
///
|
|
/// Implementations are runtime adapters over concrete TCP stream types. This
|
|
/// trait deliberately stays below tunnel framing: it only exposes stream I/O and
|
|
/// socket addresses.
|
|
pub trait VirtualTcpSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static {
|
|
/// Consumes the stream into independently owned read and write halves.
|
|
///
|
|
/// Portable adapters may use the generic shared split. Native adapters
|
|
/// should override this when their runtime provides lock-free owned halves.
|
|
fn into_split(self) -> VirtualTcpSplit
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let (reader, writer) = tokio::io::split(self);
|
|
(Box::new(reader), Box::new(writer))
|
|
}
|
|
|
|
fn local_addr(&self) -> io::Result<SocketAddr>;
|
|
|
|
fn peer_addr(&self) -> io::Result<SocketAddr>;
|
|
|
|
/// Optional host transport label retained in tunnel management metadata.
|
|
fn transport_label(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TcpSocketPurpose {
|
|
DirectConnect,
|
|
FakeTcp,
|
|
HolePunch,
|
|
ManualConnect,
|
|
ProxyNat,
|
|
StunProbe,
|
|
Socks5,
|
|
PortForward,
|
|
DataPlane,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct TcpBindOptions {
|
|
#[serde(default)]
|
|
pub context: SocketContext,
|
|
pub local_addr: Option<SocketAddr>,
|
|
pub bind_device: Option<String>,
|
|
/// `None` delegates the platform default to the host socket adapter.
|
|
pub reuse_addr: Option<bool>,
|
|
pub reuse_port: bool,
|
|
pub only_v6: bool,
|
|
}
|
|
|
|
impl TcpBindOptions {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
context: SocketContext::default(),
|
|
local_addr: None,
|
|
bind_device: None,
|
|
reuse_addr: None,
|
|
reuse_port: false,
|
|
only_v6: false,
|
|
}
|
|
}
|
|
|
|
pub fn with_local_addr(mut self, local_addr: Option<SocketAddr>) -> Self {
|
|
self.local_addr = local_addr;
|
|
self
|
|
}
|
|
|
|
pub fn with_socket_mark(mut self, socket_mark: Option<u32>) -> Self {
|
|
self.context.socket_mark = socket_mark;
|
|
self
|
|
}
|
|
|
|
pub fn with_context(mut self, context: SocketContext) -> Self {
|
|
self.context = context;
|
|
self
|
|
}
|
|
|
|
pub fn with_ip_version(mut self, ip_version: IpVersion) -> Self {
|
|
self.context.ip_version = ip_version;
|
|
self
|
|
}
|
|
|
|
pub fn with_bind_device(mut self, bind_device: Option<String>) -> Self {
|
|
self.bind_device = bind_device;
|
|
self
|
|
}
|
|
|
|
pub fn with_reuse_addr(mut self, reuse_addr: bool) -> Self {
|
|
self.reuse_addr = Some(reuse_addr);
|
|
self
|
|
}
|
|
|
|
pub fn with_reuse_port(mut self, reuse_port: bool) -> Self {
|
|
self.reuse_port = reuse_port;
|
|
self
|
|
}
|
|
|
|
pub fn with_only_v6(mut self, only_v6: bool) -> Self {
|
|
self.only_v6 = only_v6;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Default for TcpBindOptions {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct TcpConnectOptions {
|
|
pub remote_addr: SocketAddr,
|
|
pub bind: TcpBindOptions,
|
|
pub purpose: TcpSocketPurpose,
|
|
}
|
|
|
|
impl TcpConnectOptions {
|
|
pub fn direct_connect(remote_addr: SocketAddr) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default(),
|
|
purpose: TcpSocketPurpose::DirectConnect,
|
|
}
|
|
}
|
|
|
|
pub fn with_purpose(mut self, purpose: TcpSocketPurpose) -> Self {
|
|
self.purpose = purpose;
|
|
self
|
|
}
|
|
|
|
pub fn hole_punch(remote_addr: SocketAddr, local_addr: Option<SocketAddr>) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(local_addr),
|
|
purpose: TcpSocketPurpose::HolePunch,
|
|
}
|
|
}
|
|
|
|
pub fn manual_connect(remote_addr: SocketAddr, local_addr: Option<SocketAddr>) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(local_addr),
|
|
purpose: TcpSocketPurpose::ManualConnect,
|
|
}
|
|
}
|
|
|
|
pub fn proxy_nat(remote_addr: SocketAddr) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default(),
|
|
purpose: TcpSocketPurpose::ProxyNat,
|
|
}
|
|
}
|
|
|
|
pub fn stun_probe(remote_addr: SocketAddr, local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpSocketPurpose::StunProbe,
|
|
}
|
|
}
|
|
|
|
pub fn socks5(remote_addr: SocketAddr) -> Self {
|
|
Self::direct_connect(remote_addr).with_purpose(TcpSocketPurpose::Socks5)
|
|
}
|
|
|
|
pub fn port_forward(remote_addr: SocketAddr) -> Self {
|
|
Self::direct_connect(remote_addr).with_purpose(TcpSocketPurpose::PortForward)
|
|
}
|
|
|
|
pub fn data_plane(remote_addr: SocketAddr) -> Self {
|
|
Self::direct_connect(remote_addr).with_purpose(TcpSocketPurpose::DataPlane)
|
|
}
|
|
|
|
pub fn with_bind(mut self, bind: TcpBindOptions) -> Self {
|
|
self.bind = bind;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait VirtualTcpSocketFactory: Send + Sync + 'static {
|
|
type Socket: VirtualTcpSocket;
|
|
|
|
async fn connect_tcp(&self, options: TcpConnectOptions) -> anyhow::Result<Self::Socket>;
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait VirtualTcpListener: Send + Sync + 'static {
|
|
type Socket: VirtualTcpSocket;
|
|
|
|
fn local_addr(&self) -> io::Result<SocketAddr>;
|
|
|
|
async fn accept(&self) -> io::Result<(Self::Socket, SocketAddr)>;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum TcpListenPurpose {
|
|
DirectConnect,
|
|
HolePunch,
|
|
ManualConnect,
|
|
ProxyNat,
|
|
Socks5,
|
|
PortForward,
|
|
PortLease,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct TcpListenOptions {
|
|
pub bind: TcpBindOptions,
|
|
pub purpose: TcpListenPurpose,
|
|
}
|
|
|
|
impl TcpListenOptions {
|
|
pub fn direct_connect(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::DirectConnect,
|
|
}
|
|
}
|
|
|
|
pub fn hole_punch(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::HolePunch,
|
|
}
|
|
}
|
|
|
|
pub fn manual_connect(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::ManualConnect,
|
|
}
|
|
}
|
|
|
|
pub fn proxy_nat(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::ProxyNat,
|
|
}
|
|
}
|
|
|
|
pub fn socks5(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::Socks5,
|
|
}
|
|
}
|
|
|
|
pub fn port_forward(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::PortForward,
|
|
}
|
|
}
|
|
|
|
pub fn port_lease(local_addr: SocketAddr) -> Self {
|
|
Self {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::PortLease,
|
|
}
|
|
}
|
|
|
|
pub fn with_bind(mut self, bind: TcpBindOptions) -> Self {
|
|
self.bind = bind;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait VirtualTcpListenerFactory: Send + Sync + 'static {
|
|
type Listener: VirtualTcpListener;
|
|
|
|
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>>;
|
|
}
|
|
|
|
type AcceptedTcpSocket<F> =
|
|
<<F as VirtualTcpListenerFactory>::Listener as VirtualTcpListener>::Socket;
|
|
|
|
pub struct TcpSocketListener<F>
|
|
where
|
|
F: VirtualTcpListenerFactory,
|
|
{
|
|
url: url::Url,
|
|
options: TcpListenOptions,
|
|
factory: Arc<F>,
|
|
listener: Option<Arc<F::Listener>>,
|
|
}
|
|
|
|
impl<F> TcpSocketListener<F>
|
|
where
|
|
F: VirtualTcpListenerFactory,
|
|
{
|
|
pub fn new_with_options(url: url::Url, options: TcpListenOptions, factory: Arc<F>) -> Self {
|
|
Self {
|
|
url,
|
|
options,
|
|
factory,
|
|
listener: None,
|
|
}
|
|
}
|
|
|
|
fn listener(&self) -> anyhow::Result<Arc<F::Listener>> {
|
|
self.listener
|
|
.clone()
|
|
.ok_or_else(|| anyhow::anyhow!("tcp socket listener is not started"))
|
|
}
|
|
}
|
|
|
|
impl<F> fmt::Debug for TcpSocketListener<F>
|
|
where
|
|
F: VirtualTcpListenerFactory,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("TcpSocketListener")
|
|
.field("url", &self.url)
|
|
.field("options", &self.options)
|
|
.field("listening", &self.listener.is_some())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<F> SocketListener for TcpSocketListener<F>
|
|
where
|
|
F: VirtualTcpListenerFactory,
|
|
{
|
|
type Accepted = AcceptedTcpSocket<F>;
|
|
|
|
async fn listen(&mut self) -> anyhow::Result<()> {
|
|
if self.listener.is_some() {
|
|
return Ok(());
|
|
}
|
|
|
|
let listener = self.factory.bind_tcp(self.options.clone()).await?;
|
|
let local_addr = listener.local_addr()?;
|
|
self.url
|
|
.set_port(Some(local_addr.port()))
|
|
.map_err(|_| anyhow::anyhow!("failed to update tcp listener port for {}", self.url))?;
|
|
self.listener = Some(listener);
|
|
Ok(())
|
|
}
|
|
|
|
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
|
loop {
|
|
let listener = self.listener()?;
|
|
match listener.accept().await {
|
|
Ok((socket, _)) => return Ok(socket),
|
|
Err(error) if is_retryable_tcp_io_error(&error) => {
|
|
tracing::warn!(?error, "tcp accept failed with retryable error");
|
|
}
|
|
Err(error) => {
|
|
tracing::warn!(?error, "tcp accept failed");
|
|
return Err(error.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn local_url(&self) -> url::Url {
|
|
self.url.clone()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_retryable_tcp_io_error(error: &io::Error) -> bool {
|
|
use io::ErrorKind::*;
|
|
matches!(
|
|
error.kind(),
|
|
NotConnected | ConnectionAborted | ConnectionRefused | ConnectionReset
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{
|
|
collections::VecDeque,
|
|
pin::Pin,
|
|
sync::Mutex,
|
|
task::{Context, Poll},
|
|
};
|
|
|
|
use tokio::io::{DuplexStream, ReadBuf};
|
|
|
|
use super::*;
|
|
|
|
struct MockTcpSocket {
|
|
stream: DuplexStream,
|
|
local_addr: SocketAddr,
|
|
peer_addr: SocketAddr,
|
|
}
|
|
|
|
impl MockTcpSocket {
|
|
fn new(local_addr: SocketAddr, peer_addr: SocketAddr) -> Self {
|
|
let (stream, _) = tokio::io::duplex(64);
|
|
Self {
|
|
stream,
|
|
local_addr,
|
|
peer_addr,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AsyncRead for MockTcpSocket {
|
|
fn poll_read(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut Context<'_>,
|
|
buf: &mut ReadBuf<'_>,
|
|
) -> Poll<io::Result<()>> {
|
|
Pin::new(&mut self.stream).poll_read(cx, buf)
|
|
}
|
|
}
|
|
|
|
impl AsyncWrite for MockTcpSocket {
|
|
fn poll_write(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut Context<'_>,
|
|
buf: &[u8],
|
|
) -> Poll<io::Result<usize>> {
|
|
Pin::new(&mut self.stream).poll_write(cx, buf)
|
|
}
|
|
|
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
Pin::new(&mut self.stream).poll_flush(cx)
|
|
}
|
|
|
|
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
Pin::new(&mut self.stream).poll_shutdown(cx)
|
|
}
|
|
}
|
|
|
|
impl VirtualTcpSocket for MockTcpSocket {
|
|
fn local_addr(&self) -> io::Result<SocketAddr> {
|
|
Ok(self.local_addr)
|
|
}
|
|
|
|
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
|
Ok(self.peer_addr)
|
|
}
|
|
}
|
|
|
|
struct MockTcpListener {
|
|
local_addr: SocketAddr,
|
|
accepts: Mutex<VecDeque<io::Result<MockTcpSocket>>>,
|
|
}
|
|
|
|
impl MockTcpListener {
|
|
fn new(local_addr: SocketAddr, accepts: Vec<io::Result<MockTcpSocket>>) -> Self {
|
|
Self {
|
|
local_addr,
|
|
accepts: Mutex::new(accepts.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl VirtualTcpListener for MockTcpListener {
|
|
type Socket = MockTcpSocket;
|
|
|
|
fn local_addr(&self) -> io::Result<SocketAddr> {
|
|
Ok(self.local_addr)
|
|
}
|
|
|
|
async fn accept(&self) -> io::Result<(Self::Socket, SocketAddr)> {
|
|
let result = self.accepts.lock().unwrap().pop_front();
|
|
match result {
|
|
Some(Ok(socket)) => {
|
|
let peer_addr = socket.peer_addr()?;
|
|
Ok((socket, peer_addr))
|
|
}
|
|
Some(Err(error)) => Err(error),
|
|
None => std::future::pending().await,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MockTcpListenerFactory {
|
|
listener: Arc<MockTcpListener>,
|
|
binds: Mutex<Vec<TcpListenOptions>>,
|
|
}
|
|
|
|
impl MockTcpListenerFactory {
|
|
fn new(listener: Arc<MockTcpListener>) -> Self {
|
|
Self {
|
|
listener,
|
|
binds: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl VirtualTcpListenerFactory for MockTcpListenerFactory {
|
|
type Listener = MockTcpListener;
|
|
|
|
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>> {
|
|
self.binds.lock().unwrap().push(options);
|
|
Ok(self.listener.clone())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tcp_connect_options_preserve_socket_purpose() {
|
|
let remote_addr = SocketAddr::from(([127, 0, 0, 1], 11010));
|
|
let local_addr = SocketAddr::from(([0, 0, 0, 0], 0));
|
|
|
|
assert_eq!(
|
|
TcpConnectOptions::direct_connect(remote_addr),
|
|
TcpConnectOptions {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default(),
|
|
purpose: TcpSocketPurpose::DirectConnect,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::hole_punch(remote_addr, Some(local_addr)),
|
|
TcpConnectOptions {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpSocketPurpose::HolePunch,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::manual_connect(remote_addr, Some(local_addr)),
|
|
TcpConnectOptions {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpSocketPurpose::ManualConnect,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::proxy_nat(remote_addr),
|
|
TcpConnectOptions {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default(),
|
|
purpose: TcpSocketPurpose::ProxyNat,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::stun_probe(remote_addr, local_addr),
|
|
TcpConnectOptions {
|
|
remote_addr,
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpSocketPurpose::StunProbe,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::socks5(remote_addr).purpose,
|
|
TcpSocketPurpose::Socks5
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::port_forward(remote_addr).purpose,
|
|
TcpSocketPurpose::PortForward
|
|
);
|
|
assert_eq!(
|
|
TcpConnectOptions::data_plane(remote_addr).purpose,
|
|
TcpSocketPurpose::DataPlane
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tcp_listen_options_preserve_socket_purpose() {
|
|
let local_addr = SocketAddr::from(([0, 0, 0, 0], 11010));
|
|
|
|
assert_eq!(
|
|
TcpListenOptions::socks5(local_addr).purpose,
|
|
TcpListenPurpose::Socks5
|
|
);
|
|
assert_eq!(
|
|
TcpListenOptions::port_forward(local_addr).purpose,
|
|
TcpListenPurpose::PortForward
|
|
);
|
|
assert_eq!(
|
|
TcpListenOptions::port_lease(local_addr).purpose,
|
|
TcpListenPurpose::PortLease
|
|
);
|
|
|
|
assert_eq!(
|
|
TcpListenOptions::direct_connect(local_addr),
|
|
TcpListenOptions {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::DirectConnect,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpListenOptions::hole_punch(local_addr),
|
|
TcpListenOptions {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::HolePunch,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpListenOptions::manual_connect(local_addr),
|
|
TcpListenOptions {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::ManualConnect,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
TcpListenOptions::proxy_nat(local_addr),
|
|
TcpListenOptions {
|
|
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
|
|
purpose: TcpListenPurpose::ProxyNat,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tcp_bind_options_preserve_socket_configuration() {
|
|
let local_addr = SocketAddr::from(([0, 0, 0, 0], 0));
|
|
let options = TcpBindOptions::default()
|
|
.with_local_addr(Some(local_addr))
|
|
.with_socket_mark(Some(7))
|
|
.with_bind_device(Some("eth0".to_owned()))
|
|
.with_reuse_addr(true)
|
|
.with_reuse_port(true)
|
|
.with_only_v6(true);
|
|
|
|
assert_eq!(
|
|
options,
|
|
TcpBindOptions {
|
|
context: SocketContext::default().with_socket_mark(Some(7)),
|
|
local_addr: Some(local_addr),
|
|
bind_device: Some("eth0".to_owned()),
|
|
reuse_addr: Some(true),
|
|
reuse_port: true,
|
|
only_v6: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tcp_bind_default_delegates_reuse_addr_policy_to_host() {
|
|
assert_eq!(TcpBindOptions::default().reuse_addr, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tcp_socket_listener_binds_and_accepts_socket() {
|
|
let requested_addr = SocketAddr::from(([127, 0, 0, 1], 0));
|
|
let bound_addr = SocketAddr::from(([127, 0, 0, 1], 12000));
|
|
let peer_addr = SocketAddr::from(([127, 0, 0, 1], 12001));
|
|
let options = TcpListenOptions::direct_connect(requested_addr);
|
|
let listener = Arc::new(MockTcpListener::new(
|
|
bound_addr,
|
|
vec![Ok(MockTcpSocket::new(bound_addr, peer_addr))],
|
|
));
|
|
let factory = Arc::new(MockTcpListenerFactory::new(listener));
|
|
let mut socket_listener = TcpSocketListener::new_with_options(
|
|
"tcp://127.0.0.1:0".parse().unwrap(),
|
|
options.clone(),
|
|
factory.clone(),
|
|
);
|
|
|
|
socket_listener.listen().await.unwrap();
|
|
let accepted = socket_listener.accept().await.unwrap();
|
|
|
|
assert_eq!(socket_listener.local_url().port(), Some(bound_addr.port()));
|
|
assert_eq!(accepted.peer_addr().unwrap(), peer_addr);
|
|
assert_eq!(factory.binds.lock().unwrap().as_slice(), &[options]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tcp_socket_listener_retries_retryable_accept_error() {
|
|
let requested_addr = SocketAddr::from(([127, 0, 0, 1], 0));
|
|
let bound_addr = SocketAddr::from(([127, 0, 0, 1], 12010));
|
|
let peer_addr = SocketAddr::from(([127, 0, 0, 1], 12011));
|
|
let listener = Arc::new(MockTcpListener::new(
|
|
bound_addr,
|
|
vec![
|
|
Err(io::Error::new(io::ErrorKind::ConnectionReset, "reset")),
|
|
Ok(MockTcpSocket::new(bound_addr, peer_addr)),
|
|
],
|
|
));
|
|
let factory = Arc::new(MockTcpListenerFactory::new(listener));
|
|
let mut socket_listener = TcpSocketListener::new_with_options(
|
|
"tcp://127.0.0.1:0".parse().unwrap(),
|
|
TcpListenOptions::direct_connect(requested_addr),
|
|
factory,
|
|
);
|
|
|
|
socket_listener.listen().await.unwrap();
|
|
let accepted = socket_listener.accept().await.unwrap();
|
|
|
|
assert_eq!(accepted.peer_addr().unwrap(), peer_addr);
|
|
}
|
|
}
|