mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 10:56:13 -08:00
Remove quinn-plaintext to fix connection errors caused by different hash values across platforms. On x64, maintain compatibility with quinn-plaintext.
1052 lines
32 KiB
Rust
1052 lines
32 KiB
Rust
//! This example demonstrates how to make a QUIC connection that ignores the server certificate.
|
|
//!
|
|
//! Checkout the `README.md` for guidance.
|
|
|
|
use super::{FromUrl, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener};
|
|
use crate::common::global_ctx::ArcGlobalCtx;
|
|
use crate::tunnel::common::bind;
|
|
use crate::tunnel::{
|
|
TunnelInfo,
|
|
common::{FramedReader, FramedWriter, TunnelWrapper},
|
|
};
|
|
use anyhow::Context;
|
|
use derivative::Derivative;
|
|
use derive_more::{Deref, DerefMut};
|
|
use parking_lot::RwLock;
|
|
use quinn::{
|
|
ClientConfig, ConnectError, Connection, Endpoint, EndpointConfig, ServerConfig,
|
|
TransportConfig, congestion::BbrConfig, default_runtime,
|
|
};
|
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
use std::sync::OnceLock;
|
|
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
|
use tokio::net::UdpSocket;
|
|
|
|
// region config
|
|
mod crypto {
|
|
use crate::utils::BoxExt;
|
|
use bytes::{Buf, BytesMut};
|
|
use quinn_proto::crypto::{
|
|
ClientConfig, ExportKeyingMaterialError, KeyPair, Keys, ServerConfig, Session,
|
|
UnsupportedVersion,
|
|
};
|
|
use quinn_proto::transport_parameters::TransportParameters;
|
|
use quinn_proto::{
|
|
ConnectError, ConnectionId, Side, TransportError,
|
|
crypto::{CryptoError, HeaderKey, PacketKey},
|
|
};
|
|
use seahash::SeaHasher;
|
|
use std::any::Any;
|
|
use std::{hash::Hasher, sync::Arc};
|
|
use tracing::{error, instrument, trace};
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct CryptoKey;
|
|
|
|
impl CryptoKey {
|
|
fn header(self) -> KeyPair<Box<dyn HeaderKey>> {
|
|
KeyPair {
|
|
local: Box::new(self),
|
|
remote: Box::new(self),
|
|
}
|
|
}
|
|
|
|
fn packet(self) -> KeyPair<Box<dyn PacketKey>> {
|
|
KeyPair {
|
|
local: Box::new(self),
|
|
remote: Box::new(self),
|
|
}
|
|
}
|
|
|
|
fn keys(self) -> Keys {
|
|
Keys {
|
|
header: self.header(),
|
|
packet: self.packet(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HeaderKey for CryptoKey {
|
|
fn decrypt(&self, _: usize, _: &mut [u8]) {}
|
|
fn encrypt(&self, _: usize, _: &mut [u8]) {}
|
|
fn sample_size(&self) -> usize {
|
|
0
|
|
}
|
|
}
|
|
|
|
impl CryptoKey {
|
|
fn checksum(slices: &[&[u8]]) -> u64 {
|
|
let mut hasher = SeaHasher::default();
|
|
for slice in slices {
|
|
hasher.write(&(slice.len() as u64).to_le_bytes());
|
|
hasher.write(slice);
|
|
}
|
|
hasher.finish()
|
|
}
|
|
}
|
|
|
|
impl PacketKey for CryptoKey {
|
|
#[instrument(level = "trace")]
|
|
fn encrypt(&self, packet: u64, buf: &mut [u8], header_len: usize) {
|
|
let (header, rest) = buf.split_at_mut(header_len);
|
|
let (payload, tag) = rest.split_at_mut(rest.len() - self.tag_len());
|
|
let checksum = Self::checksum(&[header, payload]);
|
|
tag.copy_from_slice(&checksum.to_be_bytes());
|
|
trace!(checksum, ?header, ?payload, ?tag);
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn decrypt(
|
|
&self,
|
|
packet: u64,
|
|
header: &[u8],
|
|
payload: &mut BytesMut,
|
|
) -> Result<(), CryptoError> {
|
|
let tag = payload.split_off(payload.len() - self.tag_len()).get_u64();
|
|
trace!(tag, ?payload);
|
|
let checksum = Self::checksum(&[header, payload]);
|
|
if checksum != tag {
|
|
error!(tag, checksum, "checksum mismatch");
|
|
return Err(CryptoError);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn tag_len(&self) -> usize {
|
|
8
|
|
}
|
|
|
|
fn confidentiality_limit(&self) -> u64 {
|
|
u64::MAX
|
|
}
|
|
|
|
fn integrity_limit(&self) -> u64 {
|
|
1 << 36
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum HandshakeState {
|
|
EmitInitial,
|
|
EmitHandshake,
|
|
Done,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct QuicSession {
|
|
side: Side,
|
|
state: HandshakeState,
|
|
local: TransportParameters,
|
|
remote: Option<TransportParameters>,
|
|
}
|
|
|
|
impl QuicSession {
|
|
fn new(side: Side, params: TransportParameters) -> Self {
|
|
Self {
|
|
side,
|
|
state: HandshakeState::EmitInitial,
|
|
local: params,
|
|
remote: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Session for QuicSession {
|
|
fn initial_keys(&self, _: &ConnectionId, _: Side) -> Keys {
|
|
CryptoKey.keys()
|
|
}
|
|
|
|
fn handshake_data(&self) -> Option<Box<dyn Any>> {
|
|
self.remote.map(|params| params.boxed() as _)
|
|
}
|
|
|
|
fn peer_identity(&self) -> Option<Box<dyn Any>> {
|
|
None
|
|
}
|
|
|
|
fn early_crypto(&self) -> Option<(Box<dyn HeaderKey>, Box<dyn PacketKey>)> {
|
|
None
|
|
}
|
|
|
|
fn early_data_accepted(&self) -> Option<bool> {
|
|
Some(false)
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn is_handshaking(&self) -> bool {
|
|
self.remote.is_none() || self.state != HandshakeState::Done
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn read_handshake(&mut self, mut buf: &[u8]) -> Result<bool, TransportError> {
|
|
if self.remote.is_none() {
|
|
self.remote = Some(
|
|
TransportParameters::read(self.side, &mut buf)
|
|
.expect("failed to read transport parameters"),
|
|
);
|
|
}
|
|
Ok(true)
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn transport_parameters(&self) -> Result<Option<TransportParameters>, TransportError> {
|
|
Ok(self.remote)
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn write_handshake(&mut self, buf: &mut Vec<u8>) -> Option<Keys> {
|
|
match self.state {
|
|
HandshakeState::EmitInitial => {
|
|
if self.side.is_client() {
|
|
self.local.write(buf);
|
|
}
|
|
self.state = HandshakeState::EmitHandshake;
|
|
Some(CryptoKey.keys())
|
|
}
|
|
HandshakeState::EmitHandshake => {
|
|
if self.side.is_server() {
|
|
self.local.write(buf);
|
|
}
|
|
self.state = HandshakeState::Done;
|
|
Some(CryptoKey.keys())
|
|
}
|
|
HandshakeState::Done => None,
|
|
}
|
|
}
|
|
|
|
fn next_1rtt_keys(&mut self) -> Option<KeyPair<Box<dyn PacketKey>>> {
|
|
Some(CryptoKey.packet())
|
|
}
|
|
|
|
fn is_valid_retry(&self, _: &ConnectionId, _: &[u8], _: &[u8]) -> bool {
|
|
true
|
|
}
|
|
|
|
fn export_keying_material(
|
|
&self,
|
|
_: &mut [u8],
|
|
_: &[u8],
|
|
_: &[u8],
|
|
) -> Result<(), ExportKeyingMaterialError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CryptoConfig;
|
|
|
|
impl ClientConfig for CryptoConfig {
|
|
#[instrument(level = "trace")]
|
|
fn start_session(
|
|
self: Arc<Self>,
|
|
version: u32,
|
|
server_name: &str,
|
|
params: &TransportParameters,
|
|
) -> Result<Box<dyn Session>, ConnectError> {
|
|
Ok(Box::new(QuicSession::new(Side::Client, *params)))
|
|
}
|
|
}
|
|
|
|
impl ServerConfig for CryptoConfig {
|
|
fn initial_keys(&self, _: u32, _: &ConnectionId) -> Result<Keys, UnsupportedVersion> {
|
|
Ok(CryptoKey.keys())
|
|
}
|
|
|
|
fn retry_tag(&self, _: u32, _: &ConnectionId, _: &[u8]) -> [u8; 16] {
|
|
[0u8; 16]
|
|
}
|
|
|
|
#[instrument(level = "trace")]
|
|
fn start_session(
|
|
self: Arc<Self>,
|
|
version: u32,
|
|
params: &TransportParameters,
|
|
) -> Box<dyn Session> {
|
|
Box::new(QuicSession::new(Side::Server, *params))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn transport_config() -> Arc<TransportConfig> {
|
|
let mut config = TransportConfig::default();
|
|
|
|
config
|
|
.max_concurrent_bidi_streams(u8::MAX.into())
|
|
.max_concurrent_uni_streams(0u8.into())
|
|
.keep_alive_interval(Some(Duration::from_secs(5)))
|
|
.initial_mtu(1200)
|
|
.min_mtu(1200)
|
|
.enable_segmentation_offload(true)
|
|
.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
|
|
|
Arc::new(config)
|
|
}
|
|
|
|
pub fn server_config() -> ServerConfig {
|
|
let mut config = ServerConfig::with_crypto(Arc::new(crypto::CryptoConfig));
|
|
config.transport_config(transport_config());
|
|
config
|
|
}
|
|
|
|
pub fn client_config() -> ClientConfig {
|
|
let mut config = ClientConfig::new(Arc::new(crypto::CryptoConfig));
|
|
config.transport_config(transport_config());
|
|
config
|
|
}
|
|
|
|
pub fn endpoint_config() -> EndpointConfig {
|
|
let mut config = EndpointConfig::default();
|
|
config.max_udp_payload_size(1200).unwrap();
|
|
config
|
|
}
|
|
//endregion
|
|
|
|
//region rw pool
|
|
#[derive(Derivative)]
|
|
#[derivative(Default(bound = ""))]
|
|
#[derive(Debug, Deref, DerefMut)]
|
|
struct RwPoolInner<Item> {
|
|
#[deref]
|
|
#[deref_mut]
|
|
pool: Vec<Item>,
|
|
enabled: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct RwPool<Item> {
|
|
ephemeral: RwLock<RwPoolInner<Item>>,
|
|
persistent: RwLock<RwPoolInner<Item>>,
|
|
capacity: usize,
|
|
}
|
|
|
|
impl<Item> RwPool<Item> {
|
|
fn new(capacity: usize) -> Self {
|
|
Self {
|
|
ephemeral: RwLock::new(RwPoolInner::default()),
|
|
persistent: RwLock::new(RwPoolInner::default()),
|
|
capacity,
|
|
}
|
|
}
|
|
|
|
/// return the capacity of the ephemeral pool;
|
|
/// if `ephemeral` or `persistent` is None, read lock `self`'s pool
|
|
fn capacity(
|
|
&self,
|
|
ephemeral: Option<&RwPoolInner<Item>>,
|
|
persistent: Option<&RwPoolInner<Item>>,
|
|
) -> usize {
|
|
let guard;
|
|
let ephemeral = if let Some(ephemeral) = ephemeral {
|
|
ephemeral
|
|
} else {
|
|
guard = self.ephemeral.read();
|
|
&guard
|
|
};
|
|
|
|
let guard;
|
|
let persistent = if let Some(persistent) = persistent {
|
|
persistent
|
|
} else {
|
|
guard = self.persistent.read();
|
|
&guard
|
|
};
|
|
|
|
(self.capacity * ephemeral.enabled as usize).saturating_sub(persistent.len())
|
|
}
|
|
|
|
fn is_full(&self) -> bool {
|
|
let pool = self.ephemeral.read();
|
|
pool.len() >= self.capacity(Some(&pool), None)
|
|
}
|
|
|
|
fn is_enabled(&self) -> bool {
|
|
self.ephemeral.read().enabled
|
|
}
|
|
|
|
fn enable(&self) {
|
|
self.ephemeral.write().enabled = true;
|
|
self.resize();
|
|
}
|
|
|
|
fn disable(&self) {
|
|
self.ephemeral.write().enabled = false;
|
|
self.resize();
|
|
}
|
|
|
|
/// push an item to the persistent pool
|
|
fn push(&self, item: Item) {
|
|
self.persistent.write().push(item);
|
|
self.resize();
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
let persistent_len = self.persistent.read().len();
|
|
let ephemeral_len = self.ephemeral.read().len();
|
|
persistent_len + ephemeral_len
|
|
}
|
|
|
|
/// try to push an item to the ephemeral pool, return the item if full
|
|
fn try_push(&self, item: Item) -> Option<Item> {
|
|
let mut pool = self.ephemeral.write();
|
|
if pool.len() < self.capacity(Some(&pool), None) {
|
|
pool.push(item);
|
|
return None;
|
|
}
|
|
Some(item)
|
|
}
|
|
|
|
fn resize(&self) {
|
|
let resize = {
|
|
let pool = self.ephemeral.read();
|
|
pool.capacity() != self.capacity(Some(&pool), None)
|
|
};
|
|
if resize {
|
|
let mut pool = self.ephemeral.write();
|
|
let capacity = self.capacity(Some(&pool), None);
|
|
pool.reserve_exact(capacity);
|
|
pool.truncate(capacity);
|
|
pool.shrink_to(capacity);
|
|
}
|
|
}
|
|
|
|
fn with_iter<F, R>(&self, f: F) -> R
|
|
where
|
|
F: FnOnce(&mut dyn Iterator<Item = &Item>) -> R,
|
|
{
|
|
let ephemeral = self.ephemeral.read();
|
|
let persistent = self.persistent.read();
|
|
f(&mut persistent.iter().chain(ephemeral.iter()))
|
|
}
|
|
}
|
|
|
|
impl RwPool<Endpoint> {
|
|
fn retain_endpoints<F>(&self, mut keep: F) -> usize
|
|
where
|
|
F: FnMut(&Endpoint) -> bool,
|
|
{
|
|
let persistent_removed = {
|
|
let mut persistent = self.persistent.write();
|
|
let before = persistent.len();
|
|
persistent.retain(|endpoint| keep(endpoint));
|
|
before - persistent.len()
|
|
};
|
|
|
|
let ephemeral_removed = {
|
|
let mut ephemeral = self.ephemeral.write();
|
|
let before = ephemeral.len();
|
|
ephemeral.retain(|endpoint| keep(endpoint));
|
|
before - ephemeral.len()
|
|
};
|
|
|
|
let removed = persistent_removed + ephemeral_removed;
|
|
if removed > 0 {
|
|
self.resize();
|
|
}
|
|
removed
|
|
}
|
|
|
|
fn remove_by_local_addr(&self, local_addr: SocketAddr) -> usize {
|
|
self.retain_endpoints(|endpoint| endpoint.local_addr().ok() != Some(local_addr))
|
|
}
|
|
|
|
fn contains_local_addr(&self, local_addr: SocketAddr) -> bool {
|
|
self.persistent
|
|
.read()
|
|
.iter()
|
|
.any(|endpoint| endpoint.local_addr().ok() == Some(local_addr))
|
|
|| self
|
|
.ephemeral
|
|
.read()
|
|
.iter()
|
|
.any(|endpoint| endpoint.local_addr().ok() == Some(local_addr))
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region endpoint manager
|
|
#[derive(Debug)]
|
|
pub struct QuicEndpointManager {
|
|
ipv4: RwPool<Endpoint>,
|
|
ipv6: RwPool<Endpoint>,
|
|
both: RwPool<Endpoint>,
|
|
}
|
|
|
|
static QUIC_ENDPOINT_MANAGER: OnceLock<QuicEndpointManager> = OnceLock::new();
|
|
|
|
impl QuicEndpointManager {
|
|
fn try_create(
|
|
addr: SocketAddr,
|
|
dual_stack: bool,
|
|
socket_mark: Option<u32>,
|
|
) -> Result<Endpoint, TunnelError> {
|
|
let socket = bind::<UdpSocket>()
|
|
.addr(addr)
|
|
.only_v6(addr.is_ipv6() && !dual_stack)
|
|
.maybe_socket_mark(socket_mark)
|
|
.call()?;
|
|
let runtime = default_runtime().ok_or(TunnelError::InternalError(
|
|
"no async runtime found".to_owned(),
|
|
))?;
|
|
let mut endpoint = Endpoint::new_with_abstract_socket(
|
|
endpoint_config(),
|
|
None,
|
|
runtime.wrap_udp_socket(socket.into_std()?)?,
|
|
runtime,
|
|
)?;
|
|
endpoint.set_default_client_config(client_config());
|
|
Ok(endpoint)
|
|
}
|
|
|
|
fn create<F>(
|
|
&self,
|
|
socket_mark: Option<u32>,
|
|
mut selector: F,
|
|
) -> Result<(&RwPool<Endpoint>, Option<Endpoint>), TunnelError>
|
|
where
|
|
F: FnMut(&QuicEndpointManager) -> (&RwPool<Endpoint>, Option<(SocketAddr, bool)>),
|
|
{
|
|
loop {
|
|
let (pool, r) = selector(self);
|
|
let Some((addr, dual_stack)) = r else {
|
|
return Ok((pool, None));
|
|
};
|
|
|
|
let endpoint = Self::try_create(addr, dual_stack, socket_mark);
|
|
if let Err(error) = endpoint.as_ref()
|
|
&& dual_stack
|
|
{
|
|
tracing::warn!(?error, "create dual stack quic endpoint failed");
|
|
self.both.disable();
|
|
self.ipv4.enable();
|
|
self.ipv6.enable();
|
|
continue;
|
|
}
|
|
|
|
return Ok((pool, Some(endpoint?)));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl QuicEndpointManager {
|
|
fn new(capacity: usize) -> Self {
|
|
let ipv4 = RwPool::new(capacity.div_ceil(2));
|
|
let ipv6 = RwPool::new(capacity.div_ceil(2));
|
|
let both = RwPool::new(capacity);
|
|
both.enable();
|
|
Self { ipv4, ipv6, both }
|
|
}
|
|
|
|
fn load(global_ctx: &ArcGlobalCtx) -> &Self {
|
|
let capacity = global_ctx
|
|
.config
|
|
.get_flags()
|
|
.multi_thread
|
|
.then(std::thread::available_parallelism)
|
|
.and_then(|r| r.ok())
|
|
.map(|n| n.get())
|
|
.unwrap_or(1);
|
|
|
|
let mgr = QUIC_ENDPOINT_MANAGER.get();
|
|
match mgr {
|
|
Some(mgr) => {
|
|
for pool in [&mgr.ipv4, &mgr.ipv6, &mgr.both] {
|
|
pool.resize();
|
|
}
|
|
}
|
|
None => {
|
|
let _ = QUIC_ENDPOINT_MANAGER.set(Self::new(capacity));
|
|
}
|
|
}
|
|
|
|
QUIC_ENDPOINT_MANAGER.get().unwrap()
|
|
}
|
|
|
|
fn client_pool(&self, ip_version: IpVersion) -> &RwPool<Endpoint> {
|
|
let dual_stack = self.both.is_enabled();
|
|
match ip_version {
|
|
IpVersion::V4 if !dual_stack => &self.ipv4,
|
|
_ => {
|
|
if dual_stack {
|
|
&self.both
|
|
} else {
|
|
&self.ipv6
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get a QUIC endpoint to be used as a server
|
|
///
|
|
/// # Arguments
|
|
/// * `addr`: listen address
|
|
fn server(global_ctx: &ArcGlobalCtx, addr: SocketAddr) -> Result<Endpoint, TunnelError> {
|
|
let mgr = Self::load(global_ctx);
|
|
let socket_mark = global_ctx.config.get_flags().socket_mark;
|
|
|
|
let (pool, endpoint) = mgr.create(socket_mark, |mgr| {
|
|
let dual_stack = addr.ip() == Ipv6Addr::UNSPECIFIED && mgr.both.is_enabled();
|
|
let pool = if addr.is_ipv4() {
|
|
&mgr.ipv4
|
|
} else if dual_stack {
|
|
&mgr.both
|
|
} else {
|
|
&mgr.ipv6
|
|
};
|
|
(pool, Some((addr, dual_stack)))
|
|
})?;
|
|
|
|
let endpoint = endpoint.expect("server endpoint creation should not return None");
|
|
endpoint.set_server_config(Some(server_config()));
|
|
pool.push(endpoint.clone());
|
|
|
|
Ok(endpoint)
|
|
}
|
|
|
|
fn client_endpoint(
|
|
&self,
|
|
ip_version: IpVersion,
|
|
socket_mark: Option<u32>,
|
|
) -> Result<Endpoint, TunnelError> {
|
|
let (pool, endpoint) = self.create(socket_mark, |mgr| {
|
|
let dual_stack = mgr.both.is_enabled();
|
|
let (pool, addr) = match ip_version {
|
|
IpVersion::V4 if !dual_stack => (&mgr.ipv4, (Ipv4Addr::UNSPECIFIED, 0).into()),
|
|
_ => {
|
|
let pool = if dual_stack { &mgr.both } else { &mgr.ipv6 };
|
|
(pool, (Ipv6Addr::UNSPECIFIED, 0).into())
|
|
}
|
|
};
|
|
if pool.is_full() {
|
|
(pool, None)
|
|
} else {
|
|
(pool, Some((addr, dual_stack)))
|
|
}
|
|
})?;
|
|
|
|
if let Some(endpoint) = endpoint {
|
|
pool.try_push(endpoint);
|
|
}
|
|
|
|
Ok(pool.with_iter(|iter| iter.min_by_key(|e| e.open_connections()).unwrap().clone()))
|
|
}
|
|
|
|
fn remove_endpoint(&self, endpoint: &Endpoint) -> usize {
|
|
let Ok(local_addr) = endpoint.local_addr() else {
|
|
return 0;
|
|
};
|
|
self.remove_endpoint_by_local_addr(local_addr)
|
|
}
|
|
|
|
fn remove_endpoint_by_local_addr(&self, local_addr: SocketAddr) -> usize {
|
|
[&self.ipv4, &self.ipv6, &self.both]
|
|
.into_iter()
|
|
.map(|pool| pool.remove_by_local_addr(local_addr))
|
|
.sum()
|
|
}
|
|
|
|
fn contains_local_addr(&self, local_addr: SocketAddr) -> bool {
|
|
[&self.ipv4, &self.ipv6, &self.both]
|
|
.into_iter()
|
|
.any(|pool| pool.contains_local_addr(local_addr))
|
|
}
|
|
|
|
async fn connect(
|
|
global_ctx: &ArcGlobalCtx,
|
|
addr: SocketAddr,
|
|
) -> Result<(Endpoint, Connection), TunnelError> {
|
|
let ip_version = if addr.ip().is_ipv4() {
|
|
IpVersion::V4
|
|
} else {
|
|
IpVersion::V6
|
|
};
|
|
let socket_mark = global_ctx.config.get_flags().socket_mark;
|
|
Self::load(global_ctx)
|
|
.connect_with_ip_version(addr, ip_version, socket_mark)
|
|
.await
|
|
}
|
|
|
|
async fn connect_with_ip_version(
|
|
&self,
|
|
addr: SocketAddr,
|
|
ip_version: IpVersion,
|
|
socket_mark: Option<u32>,
|
|
) -> Result<(Endpoint, Connection), TunnelError> {
|
|
let max_endpoint_stopping_retries = self.client_pool(ip_version).len().saturating_add(1);
|
|
let mut endpoint_stopping_retries = 0;
|
|
|
|
loop {
|
|
let endpoint = self.client_endpoint(ip_version, socket_mark)?;
|
|
let connecting = match endpoint.connect(addr, "localhost") {
|
|
Ok(connecting) => connecting,
|
|
Err(ConnectError::EndpointStopping) => {
|
|
let local_addr = endpoint.local_addr().ok();
|
|
let removed = self.remove_endpoint(&endpoint);
|
|
endpoint_stopping_retries += 1;
|
|
tracing::warn!(
|
|
?addr,
|
|
?local_addr,
|
|
removed,
|
|
"removed stopped quic endpoint and retry connect"
|
|
);
|
|
if endpoint_stopping_retries > max_endpoint_stopping_retries {
|
|
return Err(anyhow::Error::new(ConnectError::EndpointStopping)
|
|
.context(format!("failed to create connection to {}", addr))
|
|
.into());
|
|
}
|
|
continue;
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow::Error::new(e)
|
|
.context(format!("failed to create connection to {}", addr))
|
|
.into());
|
|
}
|
|
};
|
|
let connection = connecting
|
|
.await
|
|
.with_context(|| format!("failed to connect to {}", addr))?;
|
|
|
|
return Ok((endpoint, connection));
|
|
}
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
struct ConnWrapper {
|
|
conn: Connection,
|
|
}
|
|
|
|
impl Drop for ConnWrapper {
|
|
fn drop(&mut self) {
|
|
self.conn.close(0u32.into(), b"done");
|
|
}
|
|
}
|
|
|
|
pub struct QuicTunnelListener {
|
|
addr: url::Url,
|
|
global_ctx: ArcGlobalCtx,
|
|
endpoint: Option<Endpoint>,
|
|
}
|
|
|
|
impl QuicTunnelListener {
|
|
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
|
QuicTunnelListener {
|
|
addr,
|
|
global_ctx,
|
|
endpoint: None,
|
|
}
|
|
}
|
|
|
|
async fn do_accept(&self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
|
// accept a single connection
|
|
let conn = self
|
|
.endpoint
|
|
.as_ref()
|
|
.unwrap()
|
|
.accept()
|
|
.await
|
|
.ok_or_else(|| anyhow::anyhow!("accept failed, no incoming"))?;
|
|
let conn = conn.await.with_context(|| "accept connection failed")?;
|
|
let remote_addr = conn.remote_address();
|
|
let (w, r) = conn.accept_bi().await.with_context(|| "accept_bi failed")?;
|
|
|
|
let arc_conn = Arc::new(ConnWrapper { conn });
|
|
|
|
let info = TunnelInfo {
|
|
tunnel_type: "quic".to_owned(),
|
|
local_addr: Some(self.local_url().into()),
|
|
remote_addr: Some(
|
|
super::build_url_from_socket_addr(&remote_addr.to_string(), "quic").into(),
|
|
),
|
|
resolved_remote_addr: Some(
|
|
super::build_url_from_socket_addr(&remote_addr.to_string(), "quic").into(),
|
|
),
|
|
};
|
|
|
|
Ok(Box::new(TunnelWrapper::new(
|
|
FramedReader::new_with_associate_data(r, 2000, Some(Box::new(arc_conn.clone()))),
|
|
FramedWriter::new_with_associate_data(w, Some(Box::new(arc_conn))),
|
|
Some(info),
|
|
)))
|
|
}
|
|
}
|
|
|
|
impl Drop for QuicTunnelListener {
|
|
fn drop(&mut self) {
|
|
let Some(endpoint) = &self.endpoint else {
|
|
return;
|
|
};
|
|
let Ok(local_addr) = endpoint.local_addr() else {
|
|
return;
|
|
};
|
|
QuicEndpointManager::load(&self.global_ctx).remove_endpoint_by_local_addr(local_addr);
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl TunnelListener for QuicTunnelListener {
|
|
async fn listen(&mut self) -> Result<(), TunnelError> {
|
|
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
|
let endpoint = QuicEndpointManager::server(&self.global_ctx, addr)?;
|
|
self.addr
|
|
.set_port(Some(endpoint.local_addr()?.port()))
|
|
.unwrap();
|
|
self.endpoint = Some(endpoint);
|
|
|
|
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) => {
|
|
tracing::warn!(?e, "accept fail");
|
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn local_url(&self) -> url::Url {
|
|
self.addr.clone()
|
|
}
|
|
}
|
|
|
|
pub struct QuicTunnelConnector {
|
|
addr: url::Url,
|
|
global_ctx: ArcGlobalCtx,
|
|
ip_version: IpVersion,
|
|
resolved_addr: Option<SocketAddr>,
|
|
}
|
|
|
|
impl QuicTunnelConnector {
|
|
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
|
QuicTunnelConnector {
|
|
addr,
|
|
global_ctx,
|
|
ip_version: IpVersion::Both,
|
|
resolved_addr: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl TunnelConnector for QuicTunnelConnector {
|
|
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
|
let addr = match self.resolved_addr {
|
|
Some(addr) => addr,
|
|
None => SocketAddr::from_url(self.addr.clone(), self.ip_version).await?,
|
|
};
|
|
let (endpoint, connection) = QuicEndpointManager::connect(&self.global_ctx, addr).await?;
|
|
|
|
let local_addr = endpoint.local_addr()?;
|
|
|
|
let (w, r) = connection
|
|
.open_bi()
|
|
.await
|
|
.with_context(|| "open_bi failed")?;
|
|
|
|
let info = TunnelInfo {
|
|
tunnel_type: "quic".to_owned(),
|
|
local_addr: Some(
|
|
super::build_url_from_socket_addr(&local_addr.to_string(), "quic").into(),
|
|
),
|
|
remote_addr: Some(self.addr.clone().into()),
|
|
resolved_remote_addr: Some(
|
|
super::build_url_from_socket_addr(&connection.remote_address().to_string(), "quic")
|
|
.into(),
|
|
),
|
|
};
|
|
|
|
let arc_conn = Arc::new(ConnWrapper { conn: connection });
|
|
Ok(Box::new(TunnelWrapper::new(
|
|
FramedReader::new_with_associate_data(r, 4500, Some(Box::new(arc_conn.clone()))),
|
|
FramedWriter::new_with_associate_data(w, Some(Box::new(arc_conn))),
|
|
Some(info),
|
|
)))
|
|
}
|
|
|
|
fn remote_url(&self) -> url::Url {
|
|
self.addr.clone()
|
|
}
|
|
|
|
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
|
self.ip_version = ip_version;
|
|
}
|
|
|
|
fn set_resolved_addr(&mut self, addr: SocketAddr) {
|
|
self.resolved_addr = Some(addr);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::common::global_ctx::tests::get_mock_global_ctx_with_network;
|
|
use crate::tunnel::{
|
|
TunnelConnector,
|
|
common::tests::{_tunnel_bench, _tunnel_pingpong},
|
|
};
|
|
use std::sync::LazyLock;
|
|
use tokio::runtime::{Builder, Runtime};
|
|
|
|
use super::*;
|
|
|
|
// Shared runtime for all tests to avoid endpoint invalidation across runtimes
|
|
static RUNTIME: LazyLock<Runtime> =
|
|
LazyLock::new(|| Builder::new_multi_thread().enable_all().build().unwrap());
|
|
|
|
fn global_ctx() -> ArcGlobalCtx {
|
|
let identity = crate::common::config::NetworkIdentity::default();
|
|
get_mock_global_ctx_with_network(Some(identity))
|
|
}
|
|
|
|
fn stopped_client_endpoint() -> (Endpoint, SocketAddr) {
|
|
let rt = Builder::new_current_thread().enable_all().build().unwrap();
|
|
let endpoint = rt.block_on(async {
|
|
QuicEndpointManager::try_create((Ipv4Addr::UNSPECIFIED, 0).into(), false, None).unwrap()
|
|
});
|
|
let local_addr = endpoint.local_addr().unwrap();
|
|
drop(rt);
|
|
assert!(matches!(
|
|
endpoint.connect("127.0.0.1:1".parse().unwrap(), "localhost"),
|
|
Err(ConnectError::EndpointStopping)
|
|
));
|
|
(endpoint, local_addr)
|
|
}
|
|
|
|
#[test]
|
|
fn quic_pingpong() {
|
|
RUNTIME.block_on(quic_pingpong_impl())
|
|
}
|
|
async fn quic_pingpong_impl() {
|
|
let listener = QuicTunnelListener::new("quic://[::]:21011".parse().unwrap(), global_ctx());
|
|
let connector =
|
|
QuicTunnelConnector::new("quic://127.0.0.1:21011".parse().unwrap(), global_ctx());
|
|
_tunnel_pingpong(listener, connector).await
|
|
}
|
|
|
|
#[test]
|
|
fn quic_bench() {
|
|
RUNTIME.block_on(quic_bench_impl())
|
|
}
|
|
async fn quic_bench_impl() {
|
|
let listener = QuicTunnelListener::new("quic://[::]:21012".parse().unwrap(), global_ctx());
|
|
let connector =
|
|
QuicTunnelConnector::new("quic://127.0.0.1:21012".parse().unwrap(), global_ctx());
|
|
_tunnel_bench(listener, connector).await
|
|
}
|
|
|
|
#[test]
|
|
fn ipv6_pingpong() {
|
|
RUNTIME.block_on(ipv6_pingpong_impl())
|
|
}
|
|
async fn ipv6_pingpong_impl() {
|
|
let listener = QuicTunnelListener::new("quic://[::1]:31015".parse().unwrap(), global_ctx());
|
|
let connector =
|
|
QuicTunnelConnector::new("quic://[::1]:31015".parse().unwrap(), global_ctx());
|
|
_tunnel_pingpong(listener, connector).await
|
|
}
|
|
|
|
#[test]
|
|
fn ipv6_domain_pingpong() {
|
|
RUNTIME.block_on(ipv6_domain_pingpong_impl())
|
|
}
|
|
async fn ipv6_domain_pingpong_impl() {
|
|
let listener = QuicTunnelListener::new("quic://[::1]:31016".parse().unwrap(), global_ctx());
|
|
let mut connector = QuicTunnelConnector::new(
|
|
"quic://test.easytier.top:31016".parse().unwrap(),
|
|
global_ctx(),
|
|
);
|
|
connector.set_ip_version(IpVersion::V6);
|
|
_tunnel_pingpong(listener, connector).await;
|
|
|
|
let listener =
|
|
QuicTunnelListener::new("quic://127.0.0.1:31016".parse().unwrap(), global_ctx());
|
|
let mut connector = QuicTunnelConnector::new(
|
|
"quic://test.easytier.top:31016".parse().unwrap(),
|
|
global_ctx(),
|
|
);
|
|
connector.set_ip_version(IpVersion::V4);
|
|
_tunnel_pingpong(listener, connector).await;
|
|
}
|
|
|
|
#[test]
|
|
fn alloc_port() {
|
|
RUNTIME.block_on(alloc_port_impl())
|
|
}
|
|
async fn alloc_port_impl() {
|
|
// v4
|
|
let mut listener =
|
|
QuicTunnelListener::new("quic://0.0.0.0:0".parse().unwrap(), global_ctx());
|
|
listener.listen().await.unwrap();
|
|
let port = listener.local_url().port().unwrap();
|
|
assert!(port > 0);
|
|
|
|
// v6
|
|
let mut listener = QuicTunnelListener::new("quic://[::]:0".parse().unwrap(), global_ctx());
|
|
listener.listen().await.unwrap();
|
|
let port = listener.local_url().port().unwrap();
|
|
assert!(port > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn listener_drop_removes_persistent_endpoint() {
|
|
RUNTIME.block_on(listener_drop_removes_persistent_endpoint_impl())
|
|
}
|
|
async fn listener_drop_removes_persistent_endpoint_impl() {
|
|
let global_ctx = global_ctx();
|
|
let endpoint_addr = {
|
|
let mut listener =
|
|
QuicTunnelListener::new("quic://127.0.0.1:0".parse().unwrap(), global_ctx.clone());
|
|
listener.listen().await.unwrap();
|
|
let endpoint_addr = listener.endpoint.as_ref().unwrap().local_addr().unwrap();
|
|
assert!(QuicEndpointManager::load(&global_ctx).contains_local_addr(endpoint_addr));
|
|
endpoint_addr
|
|
};
|
|
|
|
assert!(!QuicEndpointManager::load(&global_ctx).contains_local_addr(endpoint_addr));
|
|
}
|
|
|
|
#[test]
|
|
fn connect_removes_stopped_endpoints_and_retries() {
|
|
let (stopped_endpoint_a, stopped_addr_a) = stopped_client_endpoint();
|
|
let (stopped_endpoint_b, stopped_addr_b) = stopped_client_endpoint();
|
|
|
|
RUNTIME.block_on(async move {
|
|
let mgr = QuicEndpointManager::new(2);
|
|
mgr.both.push(stopped_endpoint_a);
|
|
mgr.both.push(stopped_endpoint_b);
|
|
assert!(mgr.contains_local_addr(stopped_addr_a));
|
|
assert!(mgr.contains_local_addr(stopped_addr_b));
|
|
|
|
let err = mgr
|
|
.connect_with_ip_version("127.0.0.1:0".parse().unwrap(), IpVersion::V4, None)
|
|
.await
|
|
.unwrap_err();
|
|
let err = format!("{:?}", err);
|
|
assert!(
|
|
err.contains("invalid remote address"),
|
|
"unexpected error: {}",
|
|
err
|
|
);
|
|
assert!(!mgr.contains_local_addr(stopped_addr_a));
|
|
assert!(!mgr.contains_local_addr(stopped_addr_b));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_peer_addr() {
|
|
RUNTIME.block_on(invalid_peer_addr_impl())
|
|
}
|
|
async fn invalid_peer_addr_impl() {
|
|
let mut connector =
|
|
QuicTunnelConnector::new("quic://127.0.0.1:0".parse().unwrap(), global_ctx());
|
|
let err = format!("{:?}", connector.connect().await.unwrap_err());
|
|
assert!(
|
|
err.contains("invalid remote address"),
|
|
"unexpected error: {}",
|
|
err
|
|
);
|
|
}
|
|
}
|