use std::{
net::{IpAddr, SocketAddr},
pin::Pin,
task::{Context, Poll},
};
use async_trait::async_trait;
use pin_project::pin_project;
use sillad::{Pipe, dialer::Dialer};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
pub fn binding_active() -> bool {
use std::sync::OnceLock;
static V: OnceLock<bool> = OnceLock::new();
*V.get_or_init(|| {
std::env::var_os("GEPH_VPN_BIND_IF4").is_some()
|| std::env::var_os("GEPH_VPN_BIND_IF6").is_some()
})
}
pub fn physical_dns_servers() -> &'static [SocketAddr] {
use std::sync::OnceLock;
static V: OnceLock<Vec<SocketAddr>> = OnceLock::new();
V.get_or_init(|| {
std::env::var("GEPH_PHYS_DNS")
.unwrap_or_default()
.split(',')
.filter_map(|s| s.trim().parse::<IpAddr>().ok())
.filter(|ip| ip.is_ipv4())
.map(|ip| SocketAddr::new(ip, 53))
.collect()
})
.as_slice()
}
pub async fn udp_socket_v4() -> std::io::Result<tokio::net::UdpSocket> {
let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
#[cfg(windows)]
windows_bind::pin_udp_v4(&socket)?;
#[cfg(target_os = "macos")]
macos_bind::pin_udp_v4(&socket)?;
Ok(socket)
}
pub struct BoundTcpDialer {
pub dest_addr: SocketAddr,
}
#[async_trait]
impl Dialer for BoundTcpDialer {
type P = BoundTcpPipe;
async fn dial(&self) -> std::io::Result<Self::P> {
let inner = loop {
match connect_bound(self.dest_addr).await {
Ok(inner) => break inner,
Err(err) if should_retry_connect(&err) => {
let _ = &err;
tracing::warn!(
addr = %self.dest_addr,
"retrying TCP connect after OS-level timeout"
);
}
Err(err) => return Err(err),
}
};
let _ = inner.set_nodelay(true);
Ok(BoundTcpPipe(inner, self.dest_addr.to_string()))
}
}
pub async fn connect_addrs(addrs: &[SocketAddr]) -> std::io::Result<BoundTcpPipe> {
let mut last_err = std::io::Error::other("no addresses to connect to");
for &dest_addr in addrs {
match (BoundTcpDialer { dest_addr }).dial().await {
Ok(pipe) => return Ok(pipe),
Err(err) => last_err = err,
}
}
Err(last_err)
}
#[pin_project]
pub struct BoundTcpPipe(#[pin] tokio::net::TcpStream, String);
impl AsyncRead for BoundTcpPipe {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
self.project().0.poll_read(cx, buf)
}
}
impl AsyncWrite for BoundTcpPipe {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.project().0.poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.project().0.poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.project().0.poll_shutdown(cx)
}
}
impl Pipe for BoundTcpPipe {
fn protocol(&self) -> &str {
"tcp"
}
fn remote_addr(&self) -> Option<&str> {
Some(&self.1)
}
}
async fn connect_bound(dest: SocketAddr) -> std::io::Result<tokio::net::TcpStream> {
#[cfg(windows)]
{
if let Some(stream) = windows_bind::connect_unicast_if(dest).await? {
return Ok(stream);
}
}
#[cfg(target_os = "macos")]
{
if let Some(stream) = macos_bind::connect_bound_if(dest).await? {
return Ok(stream);
}
}
tokio::net::TcpStream::connect(dest).await
}
#[cfg(windows)]
fn should_retry_connect(err: &std::io::Error) -> bool {
err.kind() == std::io::ErrorKind::TimedOut || err.raw_os_error() == Some(10060)
}
#[cfg(not(windows))]
fn should_retry_connect(_: &std::io::Error) -> bool {
false
}
#[cfg(windows)]
mod windows_bind {
use std::net::SocketAddr;
use std::os::windows::io::AsRawSocket;
use std::sync::OnceLock;
use windows_sys::Win32::Networking::WinSock::{SOCKET, setsockopt};
const IPPROTO_IP: i32 = 0;
const IPPROTO_IPV6: i32 = 41;
const IP_UNICAST_IF: i32 = 31;
const IPV6_UNICAST_IF: i32 = 31;
fn env_index(var: &str) -> Option<u32> {
std::env::var(var)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.filter(|&i| i != 0)
}
fn bind_if4() -> Option<u32> {
static V: OnceLock<Option<u32>> = OnceLock::new();
*V.get_or_init(|| env_index("GEPH_VPN_BIND_IF4"))
}
fn bind_if6() -> Option<u32> {
static V: OnceLock<Option<u32>> = OnceLock::new();
*V.get_or_init(|| env_index("GEPH_VPN_BIND_IF6"))
}
pub fn pin_udp_v4(socket: &tokio::net::UdpSocket) -> std::io::Result<()> {
let Some(idx) = bind_if4() else {
return Ok(());
};
let value: u32 = idx.to_be();
let rc = unsafe {
setsockopt(
socket.as_raw_socket() as SOCKET,
IPPROTO_IP,
IP_UNICAST_IF,
&value as *const u32 as *const u8,
std::mem::size_of::<u32>() as i32,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub async fn connect_unicast_if(
dest: SocketAddr,
) -> std::io::Result<Option<tokio::net::TcpStream>> {
let (level, optname, idx) = match dest {
SocketAddr::V4(_) => match bind_if4() {
Some(i) => (IPPROTO_IP, IP_UNICAST_IF, i),
None => return Ok(None),
},
SocketAddr::V6(_) => match bind_if6() {
Some(i) => (IPPROTO_IPV6, IPV6_UNICAST_IF, i),
None => return Ok(None),
},
};
let socket = match dest {
SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?,
SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?,
};
let value: u32 = if optname == IP_UNICAST_IF {
idx.to_be()
} else {
idx
};
let rc = unsafe {
setsockopt(
socket.as_raw_socket() as SOCKET,
level,
optname,
&value as *const u32 as *const u8,
std::mem::size_of::<u32>() as i32,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Some(socket.connect(dest).await?))
}
}
#[cfg(target_os = "macos")]
mod macos_bind {
use std::net::SocketAddr;
use std::os::fd::AsRawFd;
use std::sync::OnceLock;
const IPPROTO_IP: i32 = 0;
const IPPROTO_IPV6: i32 = 41;
const IP_BOUND_IF: i32 = 25;
const IPV6_BOUND_IF: i32 = 125;
fn env_index(var: &str) -> Option<u32> {
std::env::var(var)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.filter(|&i| i != 0)
}
fn bind_if4() -> Option<u32> {
static V: OnceLock<Option<u32>> = OnceLock::new();
*V.get_or_init(|| env_index("GEPH_VPN_BIND_IF4"))
}
fn bind_if6() -> Option<u32> {
static V: OnceLock<Option<u32>> = OnceLock::new();
*V.get_or_init(|| env_index("GEPH_VPN_BIND_IF6"))
}
pub fn pin_udp_v4(socket: &tokio::net::UdpSocket) -> std::io::Result<()> {
let Some(idx) = bind_if4() else {
return Ok(());
};
let value: u32 = idx;
let rc = unsafe {
libc::setsockopt(
socket.as_raw_fd(),
IPPROTO_IP,
IP_BOUND_IF,
&value as *const u32 as *const libc::c_void,
std::mem::size_of::<u32>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub async fn connect_bound_if(
dest: SocketAddr,
) -> std::io::Result<Option<tokio::net::TcpStream>> {
let (level, optname, idx) = match dest {
SocketAddr::V4(_) => match bind_if4() {
Some(i) => (IPPROTO_IP, IP_BOUND_IF, i),
None => return Ok(None),
},
SocketAddr::V6(_) => match bind_if6() {
Some(i) => (IPPROTO_IPV6, IPV6_BOUND_IF, i),
None => return Ok(None),
},
};
let socket = match dest {
SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?,
SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?,
};
let value: u32 = idx;
let rc = unsafe {
libc::setsockopt(
socket.as_raw_fd(),
level,
optname,
&value as *const u32 as *const libc::c_void,
std::mem::size_of::<u32>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Some(socket.connect(dest).await?))
}
}