mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-19 16:16:37 +00:00
EasyTier Core / pre_job (push) Canceled after 0s
EasyTier Core / build_web (push) Canceled after 0s
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-24.04, x86_64-unknown-freebsd) (push) Canceled after 0s
EasyTier Core / build (linux-aarch64, ubuntu-24.04-arm, aarch64-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (linux-arm, ubuntu-24.04, arm-unknown-linux-musleabi) (push) Canceled after 0s
EasyTier Core / build (linux-armhf, ubuntu-24.04, arm-unknown-linux-musleabihf) (push) Canceled after 0s
EasyTier Core / build (linux-armv7, ubuntu-24.04, armv7-unknown-linux-musleabi) (push) Canceled after 0s
EasyTier Core / build (linux-armv7hf, ubuntu-24.04, armv7-unknown-linux-musleabihf) (push) Canceled after 0s
EasyTier Core / build (linux-loongarch64, ubuntu-24.04, loongarch64-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (linux-mips, ubuntu-24.04, mips-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (linux-mipsel, ubuntu-24.04, mipsel-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (linux-riscv64, ubuntu-24.04, riscv64gc-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (linux-x86_64, ubuntu-24.04, x86_64-unknown-linux-musl) (push) Canceled after 0s
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Canceled after 0s
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Canceled after 0s
EasyTier Core / build (windows-arm64, windows-11-arm, aarch64-pc-windows-msvc) (push) Canceled after 0s
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Canceled after 0s
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
EasyTier Core / build_magisk (push) Canceled after 0s
EasyTier Core / core-result (push) Canceled after 0s
EasyTier GUI / pre_job (push) Canceled after 0s
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-24.04-arm, aarch64-unknown-linux-musl) (push) Canceled after 0s
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-24.04, x86_64-unknown-linux-musl) (push) Canceled after 0s
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Canceled after 0s
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Canceled after 0s
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-11-arm, aarch64-pc-windows-msvc) (push) Canceled after 0s
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Canceled after 0s
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
EasyTier GUI / gui-result (push) Canceled after 0s
EasyTier Mobile / pre_job (push) Canceled after 0s
EasyTier Mobile / build-mobile (aarch64, aarch64-linux-android) (push) Canceled after 0s
EasyTier Mobile / build-mobile (armv7, armv7-linux-androideabi) (push) Canceled after 0s
EasyTier Mobile / build-mobile (i686, i686-linux-android) (push) Canceled after 0s
EasyTier Mobile / build-mobile (x86_64, x86_64-linux-android) (push) Canceled after 0s
EasyTier Mobile / mobile-result (push) Canceled after 0s
EasyTier Test / pre_job (push) Canceled after 0s
EasyTier Test / Run linters & check (push) Canceled after 0s
EasyTier Test / Build test (push) Canceled after 0s
EasyTier Test / Test (easytier) (push) Canceled after 0s
EasyTier Test / Test (three_node) (push) Canceled after 0s
EasyTier Test / Test (three_node::subnet_proxy_three_node_test) (push) Canceled after 0s
EasyTier Test / test (push) Canceled after 0s
EasyTier JavaScript Hosts / check (push) Canceled after 0s
ohos / pre_job (push) Canceled after 0s
ohos / ohos (push) Canceled after 0s
* build(core): consolidate crypto dependencies and trim features
Align AES-GCM, ChaCha20Poly1305, HMAC, SHA2 and HKDF with the versions
already required by Snow and STUN. Align base64 with pbjson, and explain
the coordinated upgrade constraints beside the manifest entries. Adapt
AEAD and HMAC calls without changing packet or key formats, and pin the
WireGuard client key derivation with an independent HKDF vector.
Limit Snow to the Noise algorithms used by the core. Use crossbeam-utils
directly, keep the futures executor in tests, and remove UUID fast-rng
from the core while retaining existing features in native consumers.
Enable browser entropy and certificate time for the rustls backend.
Core default normal/build dependencies fall from 255 to 224 packages;
duplicated package names fall from 24 to 5 against upstream 4837468d.
Validation: Docker tests pass: 970 core, 33 proto (2 ignored), 5
WireGuard, and TCP/UDP three-node encrypted relays. Clippy passes with
warnings denied for core, proto, native and web targets. Browser
default/ChaCha20, WASI and minimal native compile checks pass. Workspace
formatting passes.
* fix(tls): select ring explicitly across application entry points
Building easytier and easytier-web together enabled both ring and aws-lc
through reqwest 0.13. HTTPS endpoint discovery used rustls automatic
provider selection and could panic before sending a ClientHello.
Use reqwest rustls-no-provider to share ring, and explicitly install the
process default before starting CLI, GUI and web services. Initialize it
for standalone webhook construction too, retaining any provider already
selected by the host.
Pass ring directly to HTTPS discovery and WebSocket TLS builders so
library use is independent of application initialization order. Keep
existing certificate verification, SNI and protocol settings.
Add a duplex-stream regression that reproduces the original panic with
both backends enabled, and run it in CI. Explain provider selection and
feature-unification constraints next to the relevant code.
Validation: the new regression fails before the fix and passes after it.
Docker tests pass: 10 discovery, 13 webhook, 2 WebSocket, WSS three-node
AES-GCM relay, and WSS credential connectivity. Clippy with warnings
denied and fmt pass. GUI, browser, WASI, minimal native, endpoint-only
and WebSocket-only compile checks pass. Linux and Windows release
dependency trees contain only the ring runtime backend.
Fixes #2607
* ci: remove the separate rustls backend regression step
Keep the HTTPS discovery regression in the existing test archive and
runner. Avoid an extra core test build solely to enable both rustls
backends; that combination was verified locally before the TLS fix.
* build(web): share reqwest 0.13 with the OIDC client
Disable the reqwest 0.12 client bundled with openidconnect/oauth2 and
use the official oauth2-reqwest adapter around the workspace reqwest
0.13 client. Pin the pre-release adapter version until its API
stabilizes.
Keep the existing redirect policy and 30-second timeout. Initialize the
ring provider when constructing OIDC configuration, including outside
the web application entry point.
Exercise discovery, JWKS fetching, token success and OAuth error
responses against a local mock provider. Verify redirects are not
followed. Refresh Cargo.lock to remove reqwest 0.12 without unrelated
upgrades.
Validation: Docker OIDC tests (2) and webhook tests (13) pass. Clippy
with warnings denied for native/core/proto/web all targets and full
features passes, as does workspace formatting. The default core/web
dependency graph has one reqwest version and 45 multi-version names
instead of 46; the TLS runtime still selects ring only.
532 lines
17 KiB
Rust
532 lines
17 KiB
Rust
use super::FromUrl;
|
|
use crate::tunnel::common::bind;
|
|
use crate::{proto::common::TunnelInfo, socket::tcp::RuntimeTcpSocket};
|
|
use anyhow::Context as _;
|
|
use bytes::BytesMut;
|
|
use cidr::IpCidr;
|
|
use easytier_core::{
|
|
packet::{ZCPacket, ZCPacketType},
|
|
socket::tcp::VirtualTcpSocket,
|
|
tunnel::{IpVersion, Tunnel, TunnelError, wrapper::TunnelWrapper},
|
|
};
|
|
use forwarded_header_value::ForwardedHeaderValue;
|
|
use futures::{Sink, StreamExt};
|
|
use std::{
|
|
net::{IpAddr, SocketAddr},
|
|
pin::Pin,
|
|
sync::{Arc, LazyLock},
|
|
task::{Context, Poll},
|
|
time::Duration,
|
|
};
|
|
use tokio::{net::TcpListener, time::timeout};
|
|
use tokio_rustls::TlsAcceptor;
|
|
use tokio_util::either::Either;
|
|
use tokio_websockets::{ClientBuilder, Limits, MaybeTlsStream, Message, ServerBuilder};
|
|
use zerocopy::AsBytes as _;
|
|
|
|
pub(crate) const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
|
pub(crate) const SERVER_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(3);
|
|
|
|
static TRUSTED_PROXIES: LazyLock<Vec<IpCidr>> = LazyLock::new(|| {
|
|
[
|
|
"127.0.0.0/8",
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
"::1/128",
|
|
"fc00::/7",
|
|
]
|
|
.into_iter()
|
|
.map(|cidr| cidr.parse().unwrap())
|
|
.collect()
|
|
});
|
|
|
|
fn trusted_proxy_contains(ip: IpAddr) -> bool {
|
|
TRUSTED_PROXIES.iter().any(|cidr| match (cidr, ip) {
|
|
(IpCidr::V4(cidr), IpAddr::V4(ip)) => cidr.contains(&ip),
|
|
(IpCidr::V6(cidr), IpAddr::V6(ip)) => cidr.contains(&ip),
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
fn websocket_error(error: impl std::fmt::Display) -> TunnelError {
|
|
TunnelError::ProtocolError(format!("websocket error: {error}"))
|
|
}
|
|
|
|
fn is_wss(url: &url::Url) -> Result<bool, TunnelError> {
|
|
match url.scheme() {
|
|
"ws" => Ok(false),
|
|
"wss" => Ok(true),
|
|
scheme => Err(TunnelError::InvalidProtocol(scheme.to_owned())),
|
|
}
|
|
}
|
|
|
|
struct WebSocketPacketSink<S> {
|
|
inner: S,
|
|
}
|
|
|
|
impl<S> WebSocketPacketSink<S> {
|
|
fn new(inner: S) -> Self {
|
|
Self { inner }
|
|
}
|
|
}
|
|
|
|
impl<S, E> Sink<ZCPacket> for WebSocketPacketSink<S>
|
|
where
|
|
S: Sink<Message, Error = E> + Unpin,
|
|
E: std::fmt::Display,
|
|
{
|
|
type Error = TunnelError;
|
|
|
|
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
Pin::new(&mut self.inner)
|
|
.poll_ready(cx)
|
|
.map(|result| result.map_err(websocket_error))
|
|
}
|
|
|
|
fn start_send(mut self: Pin<&mut Self>, packet: ZCPacket) -> Result<(), Self::Error> {
|
|
Pin::new(&mut self.inner)
|
|
.start_send(Message::binary(packet.tunnel_payload_bytes().freeze()))
|
|
.map_err(websocket_error)
|
|
}
|
|
|
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
Pin::new(&mut self.inner)
|
|
.poll_flush(cx)
|
|
.map(|result| result.map_err(websocket_error))
|
|
}
|
|
|
|
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
Pin::new(&mut self.inner)
|
|
.poll_close(cx)
|
|
.map(|result| result.map_err(websocket_error))
|
|
}
|
|
}
|
|
|
|
async fn map_from_ws_message(
|
|
message: Result<Message, tokio_websockets::Error>,
|
|
) -> Option<Result<ZCPacket, TunnelError>> {
|
|
let message = match message {
|
|
Ok(message) => message,
|
|
Err(error) => {
|
|
tracing::error!(?error, "recv from websocket error");
|
|
return Some(Err(websocket_error(error)));
|
|
}
|
|
};
|
|
if message.is_close() {
|
|
tracing::warn!("recv close message from websocket");
|
|
return None;
|
|
}
|
|
if !message.is_binary() {
|
|
let message = format!("{message:?}");
|
|
tracing::error!(?message, "Invalid packet");
|
|
return Some(Err(TunnelError::InvalidPacket(message)));
|
|
}
|
|
Some(Ok(ZCPacket::new_from_buf(
|
|
BytesMut::from(message.into_payload().as_bytes()),
|
|
ZCPacketType::DummyTunnel,
|
|
)))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SkipServerVerification(Arc<rustls::crypto::CryptoProvider>);
|
|
|
|
impl SkipServerVerification {
|
|
fn new(provider: Arc<rustls::crypto::CryptoProvider>) -> Arc<Self> {
|
|
Arc::new(Self(provider))
|
|
}
|
|
}
|
|
|
|
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
|
fn verify_server_cert(
|
|
&self,
|
|
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
|
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
|
_server_name: &rustls::pki_types::ServerName<'_>,
|
|
_ocsp: &[u8],
|
|
_now: rustls::pki_types::UnixTime,
|
|
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
|
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &rustls::pki_types::CertificateDer<'_>,
|
|
dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
rustls::crypto::verify_tls12_signature(
|
|
message,
|
|
cert,
|
|
dss,
|
|
&self.0.signature_verification_algorithms,
|
|
)
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &rustls::pki_types::CertificateDer<'_>,
|
|
dss: &rustls::DigitallySignedStruct,
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
rustls::crypto::verify_tls13_signature(
|
|
message,
|
|
cert,
|
|
dss,
|
|
&self.0.signature_verification_algorithms,
|
|
)
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
|
self.0.signature_verification_algorithms.supported_schemes()
|
|
}
|
|
}
|
|
|
|
fn get_insecure_tls_client_config() -> rustls::ClientConfig {
|
|
// Library callers may not have initialized a process-wide provider.
|
|
// Use the same explicit backend for TLS and signature verification.
|
|
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
let mut config = rustls::ClientConfig::builder_with_provider(provider.clone())
|
|
.with_safe_default_protocol_versions()
|
|
.expect("ring supports the default TLS protocol versions")
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(SkipServerVerification::new(provider))
|
|
.with_no_client_auth();
|
|
config.enable_sni = true;
|
|
config.enable_early_data = false;
|
|
config
|
|
}
|
|
|
|
fn get_insecure_tls_cert<'a>() -> (
|
|
Vec<rustls::pki_types::CertificateDer<'a>>,
|
|
rustls::pki_types::PrivateKeyDer<'a>,
|
|
) {
|
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
|
let cert_der = cert.cert.der().clone();
|
|
let private_key = cert.signing_key.serialize_der();
|
|
let private_key = rustls::pki_types::PrivatePkcs8KeyDer::from(private_key);
|
|
(vec![cert_der], private_key.into())
|
|
}
|
|
|
|
pub(crate) async fn upgrade_accepted<S>(
|
|
stream: S,
|
|
local_url: url::Url,
|
|
) -> Result<Box<dyn Tunnel>, TunnelError>
|
|
where
|
|
S: VirtualTcpSocket,
|
|
{
|
|
let peer_addr = stream.peer_addr()?;
|
|
let mut remote_url = socket_url(local_url.scheme(), peer_addr);
|
|
let stream = if is_wss(&local_url)? {
|
|
let (certificates, private_key) = get_insecure_tls_cert();
|
|
// Do not rely on another tunnel initializing the global provider.
|
|
let config = rustls::ServerConfig::builder_with_provider(Arc::new(
|
|
rustls::crypto::ring::default_provider(),
|
|
))
|
|
.with_safe_default_protocol_versions()
|
|
.with_context(|| "Failed to select TLS protocol versions")?
|
|
.with_no_client_auth()
|
|
.with_single_cert(certificates, private_key)
|
|
.with_context(|| "Failed to create server config")?;
|
|
Either::Left(TlsAcceptor::from(Arc::new(config)).accept(stream).await?)
|
|
} else {
|
|
Either::Right(stream)
|
|
};
|
|
|
|
let (request, stream) = ServerBuilder::new()
|
|
.limits(Limits::unlimited())
|
|
.max_headers(128)
|
|
.accept(stream)
|
|
.await
|
|
.map_err(websocket_error)?;
|
|
|
|
if trusted_proxy_contains(peer_addr.ip())
|
|
&& let Some(forwarded) = request
|
|
.headers()
|
|
.get("Forwarded")
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| ForwardedHeaderValue::from_forwarded(value).ok())
|
|
.or_else(|| {
|
|
request
|
|
.headers()
|
|
.get("X-Forwarded-For")
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| ForwardedHeaderValue::from_x_forwarded_for(value).ok())
|
|
})
|
|
&& let Some(ip) = forwarded.remotest_forwarded_for_ip()
|
|
{
|
|
remote_url
|
|
.set_host(Some(&ip.to_string()))
|
|
.map_err(|_| TunnelError::InvalidAddr(format!("invalid forwarded ip {ip}")))?;
|
|
remote_url
|
|
.query_pairs_mut()
|
|
.append_pair("proxy", &peer_addr.to_string());
|
|
}
|
|
|
|
let (write, read) = stream.split();
|
|
let remote_url: crate::proto::common::Url = remote_url.into();
|
|
let info = TunnelInfo {
|
|
tunnel_type: local_url.scheme().to_owned(),
|
|
local_addr: Some(local_url.into()),
|
|
remote_addr: Some(remote_url.clone()),
|
|
resolved_remote_addr: Some(remote_url),
|
|
};
|
|
Ok(Box::new(TunnelWrapper::new(
|
|
read.filter_map(map_from_ws_message),
|
|
WebSocketPacketSink::new(write),
|
|
Some(info),
|
|
)))
|
|
}
|
|
|
|
fn socket_url(scheme: &str, addr: SocketAddr) -> url::Url {
|
|
let mut url = url::Url::parse(&format!("{scheme}://0.0.0.0"))
|
|
.expect("WebSocket transport scheme should be a valid URL scheme");
|
|
url.set_ip_host(addr.ip()).unwrap();
|
|
url.set_port(Some(addr.port())).unwrap();
|
|
url
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct WsTunnelListener {
|
|
addr: url::Url,
|
|
listener: Option<TcpListener>,
|
|
socket_mark: Option<u32>,
|
|
}
|
|
|
|
impl WsTunnelListener {
|
|
pub fn new(addr: url::Url) -> Self {
|
|
WsTunnelListener {
|
|
addr,
|
|
listener: None,
|
|
socket_mark: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_socket_mark(&mut self, socket_mark: Option<u32>) {
|
|
self.socket_mark = socket_mark;
|
|
}
|
|
|
|
async fn listen_tunnel(&mut self) -> Result<(), TunnelError> {
|
|
self.listener = None;
|
|
|
|
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
|
let listener = bind::<TcpListener>()
|
|
.addr(addr)
|
|
.only_v6(true)
|
|
.maybe_socket_mark(self.socket_mark)
|
|
.call()
|
|
.await?;
|
|
|
|
self.addr
|
|
.set_port(Some(listener.local_addr()?.port()))
|
|
.unwrap();
|
|
self.listener = Some(listener);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn accept_tunnel(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
|
loop {
|
|
let listener = self.listener.as_ref().unwrap();
|
|
// only fail on tcp accept error
|
|
let (stream, _) = listener.accept().await?;
|
|
stream.set_nodelay(true).unwrap();
|
|
match timeout(
|
|
SERVER_HANDSHAKE_TIMEOUT,
|
|
upgrade_accepted(RuntimeTcpSocket::new(stream), self.addr.clone()),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Ok(tunnel)) => return Ok(tunnel),
|
|
e => {
|
|
tracing::error!(?e, ?self, "Failed to accept ws/wss tunnel");
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl easytier_core::socket::SocketListener for WsTunnelListener {
|
|
type Accepted = Box<dyn Tunnel>;
|
|
|
|
async fn listen(&mut self) -> anyhow::Result<()> {
|
|
Ok(self.listen_tunnel().await?)
|
|
}
|
|
|
|
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
|
Ok(self.accept_tunnel().await?)
|
|
}
|
|
|
|
fn local_url(&self) -> url::Url {
|
|
self.addr.clone()
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn upgrade_connected<S>(
|
|
stream: S,
|
|
remote_url: url::Url,
|
|
) -> Result<Box<dyn Tunnel>, TunnelError>
|
|
where
|
|
S: VirtualTcpSocket,
|
|
{
|
|
let is_wss = is_wss(&remote_url)?;
|
|
let local_addr = stream.local_addr()?;
|
|
let resolved_remote_addr = stream.peer_addr()?;
|
|
let info = TunnelInfo {
|
|
tunnel_type: remote_url.scheme().to_owned(),
|
|
local_addr: Some(
|
|
super::build_url_from_socket_addr(&local_addr.to_string(), remote_url.scheme()).into(),
|
|
),
|
|
remote_addr: Some(remote_url.clone().into()),
|
|
resolved_remote_addr: Some(
|
|
super::build_url_from_socket_addr(
|
|
&resolved_remote_addr.to_string(),
|
|
remote_url.scheme(),
|
|
)
|
|
.into(),
|
|
),
|
|
};
|
|
|
|
let client = ClientBuilder::from_uri(http::Uri::try_from(remote_url.to_string()).unwrap())
|
|
.max_headers(128);
|
|
let stream: MaybeTlsStream<S> = if is_wss {
|
|
let tls = tokio_rustls::TlsConnector::from(Arc::new(get_insecure_tls_client_config()));
|
|
let sni = remote_url.domain().unwrap_or("localhost").to_owned();
|
|
let server_name = rustls::pki_types::ServerName::try_from(sni)
|
|
.map_err(|_| TunnelError::InvalidProtocol("Invalid SNI".to_owned()))?;
|
|
MaybeTlsStream::Rustls(tls.connect(server_name, stream).await?)
|
|
} else {
|
|
MaybeTlsStream::Plain(stream)
|
|
};
|
|
|
|
let (client, _) = client.connect_on(stream).await.map_err(websocket_error)?;
|
|
let (write, read) = client.split();
|
|
Ok(Box::new(TunnelWrapper::new(
|
|
read.filter_map(map_from_ws_message),
|
|
WebSocketPacketSink::new(write),
|
|
Some(info),
|
|
)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use super::*;
|
|
use easytier_core::socket::SocketListener;
|
|
use futures::SinkExt;
|
|
use std::io;
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpSocket,
|
|
};
|
|
|
|
struct FailingWebSocketSink {
|
|
close_called: bool,
|
|
}
|
|
|
|
impl Sink<Message> for FailingWebSocketSink {
|
|
type Error = io::Error;
|
|
|
|
fn poll_ready(
|
|
self: Pin<&mut Self>,
|
|
_cx: &mut Context<'_>,
|
|
) -> Poll<Result<(), Self::Error>> {
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
|
|
fn start_send(self: Pin<&mut Self>, _item: Message) -> Result<(), Self::Error> {
|
|
Ok(())
|
|
}
|
|
|
|
fn poll_flush(
|
|
self: Pin<&mut Self>,
|
|
_cx: &mut Context<'_>,
|
|
) -> Poll<Result<(), Self::Error>> {
|
|
Poll::Ready(Err(io::Error::new(
|
|
io::ErrorKind::BrokenPipe,
|
|
"send failed",
|
|
)))
|
|
}
|
|
|
|
fn poll_close(
|
|
mut self: Pin<&mut Self>,
|
|
_cx: &mut Context<'_>,
|
|
) -> Poll<Result<(), Self::Error>> {
|
|
self.close_called = true;
|
|
Poll::Ready(Err(io::Error::new(
|
|
io::ErrorKind::BrokenPipe,
|
|
"close failed",
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn packet_sink_maps_send_and_close_errors_independently() {
|
|
let mut sink = WebSocketPacketSink::new(FailingWebSocketSink {
|
|
close_called: false,
|
|
});
|
|
|
|
sink.send(ZCPacket::new_with_payload(b"packet"))
|
|
.await
|
|
.expect_err("send should fail");
|
|
sink.close().await.expect_err("close should fail");
|
|
assert!(sink.inner.close_called);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ws_forwarded() {
|
|
let mut listener = WsTunnelListener::new("ws://127.0.0.1:25559".parse().unwrap());
|
|
listener.listen().await.unwrap();
|
|
|
|
let server_task = tokio::spawn(async move {
|
|
let tunnel = listener.accept().await.unwrap();
|
|
|
|
let remote_addr = tunnel
|
|
.info()
|
|
.unwrap()
|
|
.remote_addr
|
|
.unwrap()
|
|
.url
|
|
.parse::<url::Url>()
|
|
.unwrap();
|
|
|
|
assert_eq!(remote_addr.host_str().unwrap(), "203.0.113.5");
|
|
let proxy_addr = remote_addr
|
|
.query_pairs()
|
|
.find(|(k, _)| k == "proxy")
|
|
.map(|(_, v)| v.into_owned())
|
|
.unwrap();
|
|
assert_eq!(proxy_addr, "127.0.0.1:25560");
|
|
|
|
tunnel
|
|
});
|
|
|
|
let socket = TcpSocket::new_v4().unwrap();
|
|
socket.bind("127.0.0.1:25560".parse().unwrap()).unwrap();
|
|
let mut stream = socket
|
|
.connect("127.0.0.1:25559".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
|
|
let handshake = "GET / HTTP/1.1\r\n\
|
|
Host: 127.0.0.1:25559\r\n\
|
|
Upgrade: websocket\r\n\
|
|
Connection: Upgrade\r\n\
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
|
|
Sec-WebSocket-Version: 13\r\n\
|
|
X-Forwarded-For: 203.0.113.5, 192.168.1.1\r\n\
|
|
\r\n";
|
|
|
|
stream.write_all(handshake.as_bytes()).await.unwrap();
|
|
|
|
let mut buf = [0u8; 1024];
|
|
let bytes_read = stream.read(&mut buf).await.unwrap();
|
|
let response = String::from_utf8_lossy(&buf[..bytes_read]);
|
|
|
|
assert!(response.contains("101 Switching Protocols"));
|
|
|
|
let _tunnel = server_task.await.unwrap();
|
|
}
|
|
}
|