use std::io::{self, IoSlice, IoSliceMut, Read, Write};
use std::net::{Shutdown, SocketAddr, ToSocketAddrs};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use crate::accept::{Accepted, Acceptor};
use crate::driver::DriverKind;
use crate::error::{CloseReason, ConfigReason, LimitKind, NetError, TimeoutKind};
use crate::gate::KillHandle;
use crate::id::{ConnectionId, ListenerId, WorkerId};
use crate::info::{ConnectionInfo, Endpoint, Origin, Transport};
use crate::limits::{Effective, Limits};
use crate::link::{ClockRef, Got, Link, WakeSource};
use crate::opts::{ListenOptions, SocketOptions, TcpOptions};
use crate::proxy::ProxyMode;
use crate::session::Phase;
use crate::sys::{self, AddrFailure, AsSock, Interest, KillSlot, Ready};
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ListenerInfo {
pub id: ListenerId,
pub name: Option<String>,
pub local: Endpoint,
pub backlog: Option<u32>,
pub transport: Transport,
pub proxy: ProxyMode,
pub tls: bool,
}
fn effective_backlog(requested: u32) -> Option<u32> {
if cfg!(target_os = "linux") {
let cap = std::fs::read_to_string("/proc/sys/net/core/somaxconn")
.ok()
.and_then(|s| s.trim().parse::<u32>().ok());
Some(cap.map_or(requested, |c| requested.min(c)))
} else if cfg!(windows) {
Some(requested)
} else {
None
}
}
const OPTIONS_INHERITED: bool = cfg!(any(target_os = "linux", windows));
pub(crate) fn bind_error(addr: SocketAddr, e: io::Error) -> NetError {
match sys::classify_addr_error(&e) {
AddrFailure::InUse => NetError::AddrInUse { addr },
AddrFailure::NotAvailable => NetError::AddrNotAvailable { addr },
AddrFailure::Permission => NetError::PermissionDenied { what: "bind" },
_ if e.kind() == io::ErrorKind::Unsupported => NetError::Unsupported {
feature: "SO_REUSEPORT",
platform: std::env::consts::OS,
},
_ => NetError::Bind { addr, source: e },
}
}
fn connect_error(addr: SocketAddr, after: Duration, e: io::Error) -> NetError {
match sys::classify_addr_error(&e) {
AddrFailure::Refused => NetError::ConnectionRefused { addr },
AddrFailure::HostUnreachable => NetError::HostUnreachable { addr },
AddrFailure::NetworkUnreachable => NetError::NetworkUnreachable { addr },
AddrFailure::TimedOut => NetError::ConnectTimeout { addr, after },
AddrFailure::NotAvailable => NetError::AddrNotAvailable { addr },
AddrFailure::Permission => NetError::PermissionDenied { what: "connect" },
_ => NetError::Os {
op: "connect",
source: e,
},
}
}
fn resolve<A: ToSocketAddrs>(addr: A) -> Result<Vec<SocketAddr>, NetError> {
let addrs: Vec<SocketAddr> = addr
.to_socket_addrs()
.map_err(|e| NetError::Os {
op: "resolve",
source: e,
})?
.collect();
if addrs.is_empty() {
return Err(NetError::NoAddresses);
}
Ok(addrs)
}
#[derive(Debug)]
pub struct TcpListener {
inner: std::net::TcpListener,
info: ListenerInfo,
tcp: TcpOptions,
acceptor: Mutex<Acceptor>,
}
impl TcpListener {
pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<TcpListener, NetError> {
TcpListener::bind_with(addr, ListenOptions::new())
}
pub fn bind_with<A: ToSocketAddrs>(
addr: A,
options: ListenOptions,
) -> Result<TcpListener, NetError> {
let mut last = NetError::NoAddresses;
for a in resolve(addr)? {
match sys::tcp_listener(a, &options.params()) {
Ok(l) => return TcpListener::finish(l, options, ListenerId(0), None),
Err(e) => last = bind_error(a, e),
}
}
Err(last)
}
pub(crate) fn finish(
inner: std::net::TcpListener,
options: ListenOptions,
id: ListenerId,
name: Option<String>,
) -> Result<TcpListener, NetError> {
let local = inner
.local_addr()
.map_err(|e| NetError::from_os("getsockname", e))?;
if OPTIONS_INHERITED && !options.tcp.is_empty() {
options.tcp.apply(&inner, local.is_ipv6())?;
}
Ok(TcpListener {
info: ListenerInfo {
id,
name,
local: Endpoint::Ip(local),
backlog: effective_backlog(options.backlog),
transport: Transport::Tcp,
proxy: ProxyMode::Off,
tls: false,
},
inner,
tcp: options.tcp,
acceptor: Mutex::new(Acceptor::new()),
})
}
pub fn accept(&self) -> Result<(TcpConnection, ConnectionInfo), NetError> {
match self.accept_until(None)? {
Some(pair) => Ok(pair),
None => Err(NetError::TimedOut {
kind: TimeoutKind::Read,
}),
}
}
pub fn accept_timeout(
&self,
timeout: Duration,
) -> Result<Option<(TcpConnection, ConnectionInfo)>, NetError> {
self.accept_until(Some(Instant::now() + timeout))
}
fn accept_until(
&self,
deadline: Option<Instant>,
) -> Result<Option<(TcpConnection, ConnectionInfo)>, NetError> {
let mut paused_once = false;
loop {
let outcome = self
.acceptor
.lock()
.unwrap_or_else(PoisonError::into_inner)
.accept(&self.inner);
match outcome {
Accepted::Conn(stream, peer) => {
let conn = TcpConnection::accepted(stream, peer, &self.tcp, self.info.id)?;
let info = conn.info().clone();
return Ok(Some((conn, info)));
}
Accepted::Skipped(_) => continue,
Accepted::Again => {
let timeout = match deadline {
None => None,
Some(d) => {
let left = d.saturating_duration_since(Instant::now());
if left.is_zero() {
return Ok(None);
}
Some(left)
}
};
match sys::wait(self.inner.sock(), Interest::Read, None, timeout) {
Ok(Ready::TimedOut) => return Ok(None),
Ok(_) => continue,
Err(e) => return Err(NetError::from_os("poll", e)),
}
}
Accepted::Pause(backoff) => {
if paused_once {
return Err(NetError::Limit {
which: LimitKind::Descriptors,
});
}
paused_once = true;
std::thread::sleep(backoff);
}
Accepted::Failed(e) => return Err(NetError::from_os("accept", e)),
}
}
}
pub(crate) fn std(&self) -> &std::net::TcpListener {
&self.inner
}
pub(crate) fn tcp_options(&self) -> &TcpOptions {
&self.tcp
}
#[must_use]
pub fn info(&self) -> &ListenerInfo {
&self.info
}
pub fn local_addr(&self) -> Result<SocketAddr, NetError> {
self.inner
.local_addr()
.map_err(|e| NetError::from_os("getsockname", e))
}
pub fn from_std(listener: std::net::TcpListener) -> Result<TcpListener, NetError> {
match sys::is_listening_stream(listener.sock()) {
Ok(true) => {}
Ok(false) => {
return Err(NetError::InvalidConfig {
field: "socket",
reason: ConfigReason::NotListening,
});
}
Err(e) => return Err(NetError::from_os("getsockopt", e)),
}
listener
.set_nonblocking(true)
.map_err(|e| NetError::from_os("ioctl", e))?;
TcpListener::finish(
listener,
ListenOptions::new().tcp_options(TcpOptions::new()),
ListenerId(0),
None,
)
.map(|mut l| {
l.info.backlog = None;
l
})
}
#[must_use]
pub fn into_std(self) -> std::net::TcpListener {
self.inner
}
pub fn try_clone(&self) -> Result<TcpListener, NetError> {
let inner = self
.inner
.try_clone()
.map_err(|e| NetError::from_os("dup", e))?;
Ok(TcpListener {
inner,
info: self.info.clone(),
tcp: self.tcp,
acceptor: Mutex::new(Acceptor::new()),
})
}
}
#[cfg(unix)]
impl std::os::fd::AsFd for TcpListener {
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
self.inner.as_fd()
}
}
#[cfg(unix)]
impl From<TcpListener> for std::os::fd::OwnedFd {
fn from(l: TcpListener) -> std::os::fd::OwnedFd {
l.inner.into()
}
}
#[cfg(unix)]
impl TryFrom<std::os::fd::OwnedFd> for TcpListener {
type Error = NetError;
fn try_from(fd: std::os::fd::OwnedFd) -> Result<TcpListener, NetError> {
TcpListener::from_std(std::net::TcpListener::from(fd))
}
}
#[cfg(windows)]
impl std::os::windows::io::AsSocket for TcpListener {
fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
self.inner.as_socket()
}
}
#[cfg(windows)]
impl From<TcpListener> for std::os::windows::io::OwnedSocket {
fn from(l: TcpListener) -> std::os::windows::io::OwnedSocket {
l.inner.into()
}
}
#[cfg(windows)]
impl TryFrom<std::os::windows::io::OwnedSocket> for TcpListener {
type Error = NetError;
fn try_from(s: std::os::windows::io::OwnedSocket) -> Result<TcpListener, NetError> {
TcpListener::from_std(std::net::TcpListener::from(s))
}
}
#[must_use]
#[derive(Clone, Debug)]
pub struct ConnectOptions {
timeout: Duration,
local_addr: Option<SocketAddr>,
tcp: TcpOptions,
limits: Limits,
}
impl Default for ConnectOptions {
fn default() -> ConnectOptions {
ConnectOptions {
timeout: Duration::from_secs(10),
local_addr: None,
tcp: TcpOptions::new().nodelay(true),
limits: Limits::default(),
}
}
}
impl ConnectOptions {
pub fn new() -> ConnectOptions {
ConnectOptions::default()
}
pub fn timeout(mut self, total: Duration) -> ConnectOptions {
self.timeout = total;
self
}
pub fn local_addr(mut self, addr: SocketAddr) -> ConnectOptions {
self.local_addr = Some(addr);
self
}
pub fn tcp_options(mut self, options: TcpOptions) -> ConnectOptions {
self.tcp = options;
self
}
pub fn limits(mut self, limits: Limits) -> ConnectOptions {
self.limits = limits;
self
}
}
pub struct TcpConnection {
link: Option<Link>,
}
impl TcpConnection {
pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<TcpConnection, NetError> {
TcpConnection::connect_with(addr, ConnectOptions::new())
}
pub fn connect_with<A: ToSocketAddrs>(
addr: A,
options: ConnectOptions,
) -> Result<TcpConnection, NetError> {
let addrs = resolve(addr)?;
TcpConnection::connect_addrs(&addrs, &options)
}
pub fn connect_host(
host: &str,
port: u16,
options: ConnectOptions,
) -> Result<TcpConnection, NetError> {
let addrs = resolve((host, port))?;
TcpConnection::connect_addrs(&addrs, &options)
}
fn connect_addrs(
addrs: &[SocketAddr],
options: &ConnectOptions,
) -> Result<TcpConnection, NetError> {
let start = Instant::now();
let deadline = start + options.timeout;
let mut last = NetError::NoAddresses;
for (i, addr) in addrs.iter().enumerate() {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
let remaining = (addrs.len() - i) as u32;
let attempt = (left / remaining).max(Duration::from_secs(1)).min(left);
match connect_one(*addr, options, attempt) {
Ok(conn) => return Ok(conn),
Err(e) => last = e,
}
}
if matches!(last, NetError::NoAddresses) {
if let Some(addr) = addrs.first() {
last = NetError::ConnectTimeout {
addr: *addr,
after: options.timeout,
};
}
}
Err(last)
}
pub(crate) fn accepted(
stream: std::net::TcpStream,
peer: SocketAddr,
tcp: &TcpOptions,
listener: ListenerId,
) -> Result<TcpConnection, NetError> {
let local = stream
.local_addr()
.map_err(|e| NetError::from_os("getsockname", e))?;
if !OPTIONS_INHERITED && !tcp.is_empty() {
tcp.apply(&stream, local.is_ipv6())?;
}
let info = ConnectionInfo::new_tcp(
crate::id::issue_single(),
Some(listener),
WorkerId(0),
Origin::Accepted,
local,
peer,
Instant::now(),
);
let limits = Limits::default().effective(DriverKind::Blocking);
Ok(TcpConnection::from_link(Link::new(
stream,
info,
limits,
ClockRef::Monotonic,
Arc::new(KillSlot::new()),
WakeSource::Thread,
Phase::Idle,
)))
}
pub(crate) fn from_link(link: Link) -> TcpConnection {
TcpConnection { link: Some(link) }
}
pub(crate) fn take_link(&mut self) -> Option<Link> {
self.link.take()
}
fn link(&mut self) -> Result<&mut Link, NetError> {
self.link.as_mut().ok_or(NetError::Closed)
}
fn link_ref(&self) -> &Link {
match &self.link {
Some(l) => l,
None => unreachable_link(),
}
}
pub fn recv(&mut self, buf: &mut [u8]) -> Result<usize, NetError> {
let link = self.link()?;
loop {
match link.read(buf)? {
Got::Data(n) => return Ok(n),
Got::Eof => return Ok(0),
Got::Woken => continue,
}
}
}
pub fn recv_exact(&mut self, buf: &mut [u8]) -> Result<(), NetError> {
let mut filled = 0;
while filled < buf.len() {
match self.recv(&mut buf[filled..])? {
0 => return Err(NetError::Closed),
n => filled += n,
}
}
Ok(())
}
pub fn recv_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize, NetError> {
match bufs.iter_mut().find(|b| !b.is_empty()) {
Some(first) => self.recv(first),
None => Ok(0),
}
}
pub fn peek(&self, buf: &mut [u8]) -> Result<usize, NetError> {
let link = self.link_ref();
if buf.is_empty() {
return Ok(0);
}
let timeout = link.limits.read_timeout;
match sys::wait(link.stream.sock(), Interest::Read, None, Some(timeout)) {
Ok(Ready::TimedOut) => {
return Err(NetError::TimedOut {
kind: TimeoutKind::Read,
});
}
Ok(_) => {}
Err(e) => return Err(NetError::from_os("poll", e)),
}
link.stream
.peek(buf)
.map_err(|e| NetError::from_os("recv", e))
}
pub fn send(&mut self, buf: &[u8]) -> Result<usize, NetError> {
self.link()?.write(buf)
}
pub fn send_all(&mut self, buf: &[u8]) -> Result<(), NetError> {
self.link()?.write_all(buf)
}
pub fn send_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize, NetError> {
self.link()?.write_vectored(bufs)
}
pub fn send_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<(), NetError> {
self.link()?.write_all_vectored(bufs)
}
pub fn shutdown(&mut self, how: Shutdown) -> Result<(), NetError> {
self.link()?.shutdown(how)
}
pub fn close(mut self) -> Result<(), NetError> {
if let Some(link) = self.link.take() {
let reason = link.reason_or(CloseReason::SessionClosed);
link.finish(reason);
}
Ok(())
}
pub fn abort(mut self) {
if let Some(link) = self.link.take() {
let reason = link.reason_or(CloseReason::SessionError);
let reason = if reason.is_abortive() {
reason
} else {
CloseReason::SessionError
};
link.finish(reason);
}
}
#[must_use]
pub fn info(&self) -> &ConnectionInfo {
&self.link_ref().info
}
#[must_use]
pub fn id(&self) -> ConnectionId {
self.link_ref().info.id
}
#[must_use]
pub fn peer_addr(&self) -> SocketAddr {
self.link_ref()
.info
.peer
.socket_addr()
.unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0)))
}
#[must_use]
pub fn local_addr(&self) -> SocketAddr {
self.link_ref()
.info
.local
.socket_addr()
.unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0)))
}
#[must_use]
pub fn kill_handle(&self) -> KillHandle {
let link = self.link_ref();
KillHandle {
slot: link.slot.clone(),
generation: link.generation,
id: link.info.id,
}
}
pub fn set_limits(&mut self, limits: Limits) {
if let Ok(link) = self.link() {
let e: Effective = limits.effective(DriverKind::Blocking);
link.set_limits(e);
}
}
pub fn set_phase(&mut self, phase: Phase) {
if let Ok(link) = self.link() {
link.set_phase(phase);
}
}
pub fn set_exchange_deadline(&mut self, at: Option<Instant>) {
if let Ok(link) = self.link() {
link.set_exchange_deadline(at);
}
}
#[must_use]
pub fn options(&self) -> SocketOptions<'_> {
let link = self.link_ref();
SocketOptions {
stream: &link.stream,
v6: link.info.local.socket_addr().is_some_and(|a| a.is_ipv6()),
}
}
pub fn take_error(&self) -> Result<Option<io::Error>, NetError> {
sys::take_error(self.link_ref().stream.sock())
.map_err(|e| NetError::from_os("getsockopt", e))
}
}
#[cold]
fn unreachable_link() -> ! {
std::process::abort()
}
fn connect_one(
addr: SocketAddr,
options: &ConnectOptions,
attempt: Duration,
) -> Result<TcpConnection, NetError> {
let owned = sys::tcp_socket(addr.is_ipv6()).map_err(|e| NetError::from_os("socket", e))?;
let stream = std::net::TcpStream::from(owned);
stream
.set_nonblocking(true)
.map_err(|e| NetError::from_os("ioctl", e))?;
options.tcp.apply(&stream, addr.is_ipv6())?;
if let Some(local) = options.local_addr {
sys::bind_to(stream.sock(), local).map_err(|e| bind_error(local, e))?;
}
let started = Instant::now();
match sys::connect_start(stream.sock(), addr) {
Ok(false) => {}
Ok(true) => {
match sys::wait(stream.sock(), Interest::Write, None, Some(attempt)) {
Ok(Ready::TimedOut) => {
return Err(NetError::ConnectTimeout {
addr,
after: attempt,
});
}
Ok(_) => {}
Err(e) => return Err(NetError::from_os("poll", e)),
}
match sys::take_error(stream.sock()) {
Ok(None) => {}
Ok(Some(e)) => return Err(connect_error(addr, started.elapsed(), e)),
Err(e) => return Err(NetError::from_os("getsockopt", e)),
}
}
Err(e) => return Err(connect_error(addr, started.elapsed(), e)),
}
sys::prepare_dialed(&stream).map_err(|e| NetError::from_os("ioctl", e))?;
let local = stream
.local_addr()
.map_err(|e| NetError::from_os("getsockname", e))?;
let info = ConnectionInfo::new_tcp(
crate::id::issue_single(),
None,
WorkerId(0),
Origin::Dialed,
local,
crate::addr::normalize_socket(addr),
Instant::now(),
);
Ok(TcpConnection::from_link(Link::new(
stream,
info,
options.limits.effective(DriverKind::Blocking),
ClockRef::Monotonic,
Arc::new(KillSlot::new()),
WakeSource::Thread,
Phase::Receiving,
)))
}
impl Read for TcpConnection {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.recv(buf).map_err(io::Error::from)
}
}
impl Write for TcpConnection {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.send(buf).map_err(io::Error::from)
}
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
self.send_vectored(bufs).map_err(io::Error::from)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Drop for TcpConnection {
fn drop(&mut self) {
if let Some(link) = self.link.take() {
let reason = link.reason_or(CloseReason::SessionClosed);
link.finish_quick(reason);
}
}
}
impl std::fmt::Debug for TcpConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.link {
Some(l) => f
.debug_struct("TcpConnection")
.field("info", &l.info)
.finish(),
None => f.write_str("TcpConnection(closed)"),
}
}
}
#[cfg(unix)]
impl std::os::fd::AsFd for TcpConnection {
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
std::os::fd::AsFd::as_fd(&self.link_ref().stream)
}
}
#[cfg(windows)]
impl std::os::windows::io::AsSocket for TcpConnection {
fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
std::os::windows::io::AsSocket::as_socket(&self.link_ref().stream)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_listen_accept_echo_round_trip() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
let t = std::thread::spawn(move || {
let (mut c, info) = l.accept().unwrap();
assert_eq!(info.origin, Origin::Accepted);
let mut buf = [0u8; 4];
c.recv_exact(&mut buf).unwrap();
c.send_all(&buf).unwrap();
c.close().unwrap();
});
let mut c = TcpConnection::connect(addr).unwrap();
c.send_all(b"ping").unwrap();
let mut buf = [0u8; 4];
c.recv_exact(&mut buf).unwrap();
assert_eq!(&buf, b"ping");
assert_eq!(c.recv(&mut buf).unwrap(), 0);
t.join().unwrap();
}
#[test]
fn test_connect_refused_maps_to_connection_refused() {
let port = std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let err = TcpConnection::connect_with(
("127.0.0.1", port),
ConnectOptions::new().timeout(Duration::from_secs(5)),
)
.unwrap_err();
assert!(
matches!(
err,
NetError::ConnectionRefused { .. } | NetError::ConnectTimeout { .. }
),
"{err:?}"
);
}
#[test]
fn test_bind_in_use_maps_to_addr_in_use() {
let a = TcpListener::bind("127.0.0.1:0").unwrap();
let err = TcpListener::bind(a.local_addr().unwrap()).unwrap_err();
assert!(matches!(err, NetError::AddrInUse { .. }), "{err:?}");
}
#[test]
fn test_bind_errors_map_by_class() {
#[cfg(unix)]
use crate::sys::errno::{EACCES as ACCESS, EADDRNOTAVAIL as NOT_AVAILABLE};
#[cfg(windows)]
use crate::sys::errno::{WSAEACCES as ACCESS, WSAEADDRNOTAVAIL as NOT_AVAILABLE};
let addr: SocketAddr = "[::1]:80".parse().unwrap();
let e = bind_error(addr, io::Error::from_raw_os_error(NOT_AVAILABLE));
assert!(matches!(e, NetError::AddrNotAvailable { .. }), "{e:?}");
let e = bind_error(addr, io::Error::from_raw_os_error(ACCESS));
assert!(matches!(e, NetError::PermissionDenied { .. }), "{e:?}");
}
#[test]
fn test_read_timeout_fires() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
let _server = std::thread::spawn(move || {
let (c, _) = l.accept().unwrap();
std::thread::sleep(Duration::from_secs(2));
drop(c);
});
let limits = Limits::builder()
.read_timeout(Duration::from_millis(200))
.min_rate(crate::limits::MinRate::off())
.build()
.unwrap();
let mut c =
TcpConnection::connect_with(addr, ConnectOptions::new().limits(limits)).unwrap();
let mut buf = [0u8; 8];
let start = Instant::now();
let err = c.recv(&mut buf).unwrap_err();
assert!(
matches!(
err,
NetError::TimedOut {
kind: TimeoutKind::Read
}
),
"{err:?}"
);
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn test_kill_wakes_blocked_recv() {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
let _server = std::thread::spawn(move || {
let (c, _) = l.accept().unwrap();
std::thread::sleep(Duration::from_secs(3));
drop(c);
});
let mut c = TcpConnection::connect(addr).unwrap();
let kill = c.kill_handle();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
assert!(kill.kill(crate::error::KillReason(4)));
});
let mut buf = [0u8; 8];
let start = Instant::now();
let err = c.recv(&mut buf).unwrap_err();
assert!(matches!(err, NetError::Killed { .. }), "{err:?}");
assert!(start.elapsed() < Duration::from_secs(2));
}
}