跳转到内容

Link、connector 与网络类型

源码文件:50 个 · 核对版本 Etemenanki 596916d · katana v3.0.1
  • Etemenanki/concepts/src/link.rs
  • Etemenanki/concepts/src/net.rs
  • Etemenanki/concepts/src/sniff.rs
  • Etemenanki/concepts/src/relay.rs
  • Etemenanki/concepts/src/core.rs
  • Etemenanki/concepts/src/runtime.rs
  • Etemenanki/concepts/src/client.rs
  • Etemenanki/concepts/tests/runtime.rs
  • Etemenanki/concepts/tests/client.rs
  • Etemenanki/environment/src/dial/mod.rs
  • Etemenanki/environment/src/dial/tcp.rs
  • Etemenanki/environment/src/dial/udp.rs
  • Etemenanki/environment/src/routing.rs
  • Etemenanki/environment/tests/integration/udp.rs
  • Etemenanki/protocols/src/flow.rs
  • Etemenanki/protocols/src/core/mod.rs
  • Etemenanki/protocols/src/sniff/mod.rs
  • Etemenanki/protocols/src/socks/handshake.rs
  • Etemenanki/protocols/src/socks/server.rs
  • Etemenanki/protocols/src/socks/protocol.rs
  • Etemenanki/protocols/src/socks/udp_link.rs
  • Etemenanki/protocols/src/http/core.rs
  • Etemenanki/protocols/src/trojan/core.rs
  • Etemenanki/protocols/src/ss_legacy/core.rs
  • Etemenanki/protocols/src/ss_2022/core.rs
  • Etemenanki/protocols/src/vless/core.rs
  • Etemenanki/protocols/src/vmess/core.rs
  • Etemenanki/protocols/src/hysteria/server/inbound.rs
  • Etemenanki/protocols/src/tun/inbound.rs
  • Etemenanki/protocols/src/transports/connect.rs
  • Etemenanki/app/src/connector.rs
  • Etemenanki/app/src/router.rs
  • Etemenanki/app/src/outbound/freedom.rs
  • Etemenanki/app/src/outbound/proxy.rs
  • Etemenanki/app/src/outbound/udp_fanout.rs
  • Etemenanki/app/src/flow.rs
  • Etemenanki/protocols/src/mux/demux.rs
  • Etemenanki/protocols/src/hysteria/server/authenticator.rs
  • Etemenanki/protocols/tests/pipeline/transports.rs
  • Etemenanki/protocols/tests/pipeline/socks.rs
  • Etemenanki/protocols/tests/unit/socks/server.rs
  • Etemenanki/protocols/tests/unit/socks/protocol.rs
  • Etemenanki/app/tests/integration/e2e_sniff.rs
  • katana/src/connector.rs
  • katana/src/router.rs
  • katana/src/traffic.rs
  • katana/src/outbound/proxy.rs
  • katana/tests/unit/serve.rs
  • katana/tests/unit/e2e.rs
  • katana/tests/unit/runtime.rs

etemenanki-concepts 之上的每个 crate 都使用同一套小而共享的词汇。link.rs 规定出站是什么、如何拨号;net.rs 规定地址和用户是什么;sniff.rs 规定嗅探得到了什么;relay.rs 为两端之间没有协议的路径提供两个字节拷贝 future。这些模块都不解析域名、不施加 socket 策略,也不做路由。它们只固定形状,供服务端运行时、客户端运行时、拨号器、协议核心、app 和 katana 接入。

本页面向新增协议、出站类型或 connector,或者修改运行时对待 link 方式的贡献者。它给出每个类型的准确签名,说明由谁实现、由谁使用,并把每条规则对应到强制执行它的机制以及固定它的测试。

模块 定义 留给其他部分
concepts/src/link.rs DatagramLink、UdpOutbound、Outbound<S, D>、Connector、SocketConnector、SocketTarget、NoStream、NoDatagram socket 选项、连接超时和 DNS。这些由 etemenanki-environment 中的拨号器和应用层出站负责。
concepts/src/net.rs UserAuthorization、NetworkUser<T>、DialNetwork、Remote、Destination 域名解析。Destination 可以是一个域名,由最终去连接它的一方决定如何解析。
concepts/src/sniff.rs SniffedProtocol、SniffedBehavior、Sniffer 嗅探器(TlsSniffer、HttpSniffer)及其字节预算和截止时间。它们位于 protocols/src/sniff/。
concepts/src/relay.rs UnidirectionalConnection、BidirectionalConnection、Relayed 超时和计量。由调用方包装这个 future。

protocols/src/flow.rs 中的 Flow<T> 不属于 concepts crate。它出现在本页,是因为 etemenanki-protocols 中的每个服务端协议核心都把它作为 Target 交给 connector,因此它正是网络类型与嗅探类型的交汇点:

protocols/src/flow.rs
pub struct Flow<T> {
pub destination: Destination,
pub user: NetworkUser<T>,
pub sniffed: Option<SniffedBehavior>,
pub source: Option<IpAddr>,
}
impl<T> Flow<T> {
pub fn new(destination: Destination, user: NetworkUser<T>, source: Option<IpAddr>) -> Self;
pub fn toward(&self, destination: Destination) -> Self;
}
impl<T> Clone for Flow<T>;

服务端协议核心决定打开什么。运行时请 Connector 去打开它,并拿回一个 Outbound:要么是字节流,要么是 DatagramLink。此后,运行时在协议核心与该 link 之间搬运字节。

flowchart LR
  wire["客户端传输层"] --> rt["ProxyServerRuntime"]
  rt -- "Event" --> core["ProxyCoreDecode"]
  core -- "Effect::Open, target = Flow" --> rt
  sn["嗅探结果:SniffedBehavior"] -.-> |"Flow.sniffed"| core
  rt -- "Connector::connect(target)" --> conn["Connector"]
  conn -- "Outbound::Stream" --> s["AsyncRead + AsyncWrite + Unpin"]
  conn -- "Outbound::Datagram" --> d["DatagramLink, Addr = Destination"]
  s --> dest["目的地或上游"]
  d --> dest

同样的 trait 在下一层再次出现。ProxyClientConnector 是一个 Connector,它的出站是 ProxyClientRuntime,而每个 ProxyClientRuntime 又通过另一个 Connector 拨号到上游:在 app 和 katana 中都是 TransportConnector。因此,一条被路由到代理出站的连接在同一个 task 内运行三个 connector:路由 connector(AppConnector 或 KatanaConnector)、该出站的 ProxyClientConnector,以及它的 TransportConnector。

层 实现者 Target Stream / Datagram
App 路由 app/src/connector.rs → AppConnector Flow(app 的 Flow<()>) OutboundStream / FanOutLink
katana 路由 src/connector.rs → KatanaConnector Flow<UserTag> Metered<OutboundStream> / FanOut
直连出站 app/src/outbound/freedom.rs → FreedomConnector Flow TcpStream / ResolvingUdp
代理出站 concepts/src/client.rs → ProxyClientConnector 由其 Make 闭包的参数决定:app 中是 Flow,katana 中是 Destination ProxyClientRuntime<BUF_SIZE, S, Conn> / ProxyClientRuntime<BUF_SIZE, D, Conn>
上游拨号 protocols/src/transports/connect.rs → TransportConnector Destination TransportStream / NoDatagram
隧道出站 protocols/src/wireguard/connector.rs → WgConnector,protocols/src/hysteria/connector.rs → Hy2Connector Flow<T> WgStream / WgDatagramLink,Hy2Stream / Hy2DatagramLink
主机 socket environment/src/dial/mod.rs → Dialer DialTarget 或 SocketTarget TcpStream / DualStackUdp 或 UdpOutbound
参考实现与测试 concepts/src/link.rs → SocketConnector,以及任意闭包 任意 任意
concepts/src/link.rs
pub trait DatagramLink: Unpin {
type Addr;
fn poll_send_to(
&mut self,
cx: &mut Context<'_>,
buf: &[u8],
to: &Self::Addr,
) -> Poll<io::Result<usize>>;
fn poll_recv_from(
&mut self,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<Self::Addr>>;
}

数据报 link 面向数据包。一次 poll_send_to 就是向 to 发送一个数据报,一次 poll_recv_from 就是从它返回的地址收到一个数据报。它没有流结束、没有 flush,也没有 shutdown。方法接收 &mut self 而不是 Pin<&mut Self>,并且 trait 要求 Unpin:

  • 服务端运行时把出站按值存放在 BTreeMap 中,map 重新平衡时会移动它们。Unpin 保证这样做是安全的,link.rs 的模块文档也以此作为该约束的理由。
  • 一旦固定 Addr,该 trait 仍然是 object safe 的。app/src/outbound/proxy.rs → OutboundDatagram 包装了一个 Box<dyn DatagramLink<Addr = Destination> + Send>,katana 的 src/outbound/proxy.rs 中也有同名类型。

Addr 是数据包的寻址方式,它决定了 link 可以扮演两种角色中的哪一种:

角色 要求的 Addr 由谁强制 实现者
出站,位于 Connector 之后 Destination 负责 poll 它的那些 ProxyServerRuntime impl(包括其 Future 和 Stream impl)上的 Conn::Datagram: DatagramLink<Addr = Destination> UdpOutbound、DualStackUdp、ResolvingUdp、FanOutLink、OutboundDatagram、BlackholeLink、SocksUdpLink<S>、WgDatagramLink、Hy2DatagramLink、基于数据报 codec 的 ProxyClientRuntime、katana 的 FanOut
传输层,即用 over_datagrams 构建的运行时的客户端一侧 协议核心的 TransportAddr ProxyServerRuntime::over_datagrams 上的 D: DatagramLink<Addr = Core::TransportAddr> TunUdpLink(SocketAddr,TUN 的 UDP 路径)、QuicDatagrams((),一条 Hysteria 2 QUIC 连接的数据报)、UdpSocket(SocketAddr,供 concepts 测试使用)

运行时中这两个约束的写法如下:

concepts/src/runtime.rs
impl<const BUF_SIZE: usize, Core, D, Conn>
ProxyServerRuntime<BUF_SIZE, Core, DatagramTransport<D>, Conn, ProxyRunsQuiet>
where
Core: ProxyCoreDecode,
D: DatagramLink<Addr = Core::TransportAddr>,
Conn: Connector<Core::Target>,
{
pub fn over_datagrams(link: D, core: Core, connector: Conn) -> Self;
}
impl<const BUF_SIZE: usize, Core, Trans, Conn> Future
for ProxyServerRuntime<BUF_SIZE, Core, Trans, Conn, ProxyRunsQuiet>
where
Core: ProxyCoreDecode,
Trans: Transport<Addr = Core::TransportAddr>,
Conn: Connector<Core::Target>,
Conn::Datagram: DatagramLink<Addr = Destination>,
{
type Output = Result<Traffic, RuntimeError<Core::Error>>;
}

在两种角色下,运行时对 link 返回结果的解读不同。link 的作者需要知道哪些结果会让 link 保持存活:

调用与结果 作为出站 作为传输层
poll_send_to → Pending SendTo effect 留在队首,并阻塞其后的所有 effect。该 key 的 waker 会让它恢复执行。 数据包保持暂存。
poll_send_to → Ready(Ok(n)) 数据包发送完成,n 计入 Traffic::outbound_tx。短写不会重试。 数据包发送完成。
poll_send_to → Ready(Err(e)) 丢弃该数据包,key 保持存活。协议核心收到 Event::SendFailed { key, to, error }。 丢弃该数据包,协议核心收到 Event::TransportSendFailed { to, error }。运行时继续运行。
poll_recv_from → Ready(Ok(from)) Event::Datagram { key, from, data },一个完整的数据包。 Event::TransportDatagram { from, data },一个完整的数据包。
poll_recv_from → Ready(Err(e)) 移除该 key,协议核心收到 Event::OutboundError { key, error }。 致命错误:运行时以 RuntimeError::Transport 结束。

运行时从数据报出站读取一个数据包时,最多读取 datagram_limit() 字节:即协议核心的 MAX_DATAGRAM(默认 4096),上限为 BUF_SIZE - STAGING_RESERVE。从数据报传输层读取时,最多读取 MAX_DATAGRAM 字节,上限为 BUF_SIZE。在两种角色下,更长的数据包都会被截断,就像内核的 recv 那样。只有当 staging 缓冲区有 STAGING_RESERVE + datagram_limit() 字节空闲时,运行时才会 poll 数据报出站,因此停止读取的客户端会让下行在 socket 处停住,而不是丢失数据包的尾部。调度细节见服务端运行时页面。

有意丢弃数据包的 link 返回 Ready(Ok(buf.len()))。BlackholeLink 对每个数据包都这样做,ResolvingUdp 对无法解析的域名也这样做。改为返回错误的 link 则把如何处理交给协议核心决定。

concepts/src/link.rs
impl DatagramLink for UdpSocket {
type Addr = SocketAddr;
// poll_send_to -> UdpSocket::poll_send_to(self, cx, buf, *to)
// poll_recv_from -> UdpSocket::poll_recv_from(self, cx, buf)
}

裸的 tokio socket 直接转发给它的固有方法。它的 Addr 是 SocketAddr,因此只适合传输层角色:一个 socket 服务多个对端,每个对端由具体地址标识。它不能在服务端运行时下作为 connector 的 Datagram 类型,因为运行时在那里要求 Addr = Destination。

concepts/src/link.rs
pub struct UdpOutbound(pub UdpSocket);
impl DatagramLink for UdpOutbound {
type Addr = Destination;
}

UdpOutbound 是扮演出站角色的同一个 socket。poll_send_to 调用 Destination::socket_addr():

  • 对于 Remote::IpAddr,发送到该地址。
  • 对于 Remote::Domain,返回 ErrorKind::Unsupported,文本为 a plain UDP outbound cannot resolve a domain。运行时把它转为 Event::SendFailed,key 保持存活。

poll_recv_from 用 Destination::udp 包装对端的 SocketAddr,因此对这个 link 来说,Event::Datagram 的 from 总是 IP。

这种拒绝是有意为之。解析属于策略(由哪个解析器应答、目的地可以使用哪个地址族),而策略属于应用层。environment/src/dial/udp.rs → DualStackUdp 做了同样的选择,文本为 udp: a plain dual-stack link cannot resolve a domain。负责解析的 link 是 app/src/outbound/freedom.rs → ResolvingUdp,它通过 app 的 Resolver 查询域名,详见 app 出站页面。

concepts/src/link.rs
pub enum Outbound<S, D> {
Stream(S),
Datagram(D),
}

Outbound<S, D> 是一个已拨通的出站。变体由 connector 选择,而不是由调用方选择。对于 destination.network 为 DialNetwork::Udp 的流,AppConnector 和 KatanaConnector 不拨号就直接返回 Datagram,其他情况返回 Stream。(KatanaConnector 会先对该流的用户做准入,对已不再注册的用户让拨号失败。)只接受一种类型的使用方会显式拒绝另一种:

使用方 收到 结果
ProxyServerRuntime,执行 Effect::Forward 或 Effect::ForwardHeld 时 数据报出站 RuntimeError::WrongLinkKind(stream effect on a datagram outbound or vice versa),结束该连接
ProxyServerRuntime,执行 Effect::SendTo 或 Effect::SendToHeld 时 流出站 RuntimeError::WrongLinkKind
ProxyServerRuntime,执行 Effect::Shutdown 时 数据报出站 不是错误:该 effect 立即完成,不调用 link,link 保持打开
ProxyClientRuntime,拨号上游线路时 Outbound::Datagram ErrorKind::Unsupported(proxy client runtime needs a stream to the upstream, got a datagram socket),线路变为 Down
protocols/src/socks/server.rs,CONNECT 请求 Outbound::Datagram ErrorKind::Unsupported(socks: a CONNECT was answered with a datagram link)
protocols/src/socks/server.rs,UDP ASSOCIATE 请求 Outbound::Stream ErrorKind::Unsupported(socks: an association was answered with a stream)
app/src/outbound/proxy.rs 和 katana 的 src/outbound/proxy.rs 中的 proxy_stream Outbound::Datagram ErrorKind::Other(a TCP flow was dialed as datagrams)
同样两个文件中的 proxy_datagram Outbound::Stream ErrorKind::Other(a UDP flow was dialed as a stream)

这个枚举也被当作普通的“二选一”类型使用。ProxyClientConnector 的 Make 闭包返回 Outbound<S, D>,其中 S 和 D 是 codec 而不是 link:它为一条流选择 stream codec 或数据报 codec。app 和 katana 都有各自名为 outbound::Outbound 的类型,因此它们导入 concepts 的模块,并把这个类型写作 link::Outbound。

concepts/src/link.rs
pub trait Connector<Target> {
type Stream: AsyncRead + AsyncWrite + Unpin;
type Datagram: DatagramLink;
type Future: Future<Output = io::Result<Outbound<Self::Stream, Self::Datagram>>>;
fn connect(&mut self, target: Target) -> Self::Future;
}

Connector 把一个 Target 变成一个 Outbound。在服务端运行时下,Target 是协议核心的 ProxyCoreDecode::Target,对 etemenanki-protocols 中的每个协议核心来说都是 Flow<T>。在客户端运行时下,它是 codec 的 ProxyCoreEncodeHandshake::Target:即上游服务器,在 app 中是一个 Destination。

该 trait 的以下特性决定了它周边代码的写法:

  • connect 接收 &mut self,因此 connector 可以在多次拨号之间保存状态。服务端运行时在执行 Effect::Open 时同步调用它。ProxyClientRuntime::new 只在创建拨号 future 的那次调用中借用 connector(connector: &mut Conn)。
  • Future 没有 Unpin 或 Send 约束。两个运行时都会在每次打开时把它 box 一次:服务端运行时中是 LinkState::Connecting(Pin<Box<F>>),客户端运行时中是 Wire::Connecting(Pin<Box<F>>)。因此 connector 可以返回匿名的 async 类型或 std::future::Ready。除 ProxyClientConnector 外,每个生产环境 connector 都返回 Pin<Box<dyn Future<Output = …> + Send>>,以便持有它的运行时可以被 spawn。ProxyClientConnector 返回具名 future ProxyClientConnecting,它只在上游拨通且 codec 握手完成后才 resolve。
  • future 的错误就是该出站的失败。服务端运行时会移除该 key,丢弃仍为它排队的所有 effect(forget_key),并投递 Event::ConnectFailed { key, error }。
concepts/src/link.rs
impl<Target, F, Fut, S, D> Connector<Target> for F
where
F: FnMut(Target) -> Fut,
Fut: Future<Output = io::Result<Outbound<S, D>>>,
S: AsyncRead + AsyncWrite + Unpin,
D: DatagramLink,
{
type Stream = S;
type Datagram = D;
type Future = Fut;
}

任何 future 产出 io::Result<Outbound<S, D>> 的 FnMut(Target) -> Fut 都是 connector,因此测试永远不需要为它命名类型。这包括普通函数(fn 项本身就是 FnMut),以及借助标准库为 &mut F 提供的 FnMut impl 对闭包的可变借用。concepts 测试使用闭包和普通函数:

concepts/tests/runtime.rs
fn udp_connector(_: ()) -> Ready<io::Result<Outbound<NoStream, UdpOutbound>>>
fn dialer(dials: Dials) -> impl FnMut(String) -> DialFuture

concepts/tests/client.rs 中的 dial_failure_surfaces_on_first_use 把闭包 dial 用作 connector,并传入 &mut dial,因为 ProxyClientRuntime::new 接收 connector: &mut Conn。katana 的测试用同样的方式构建仅支持流的 connector:tests/unit/serve.rs 中是一个闭包,tests/unit/e2e.rs 中是具名 connector 类型,二者都是 Datagram = NoDatagram。其中的具名类型 TcpConnector(一个 Connector<Destination>)同时也是 VMess 和 VLESS 测试客户端(vmess_client、vless_client)的 connector,tests/unit/runtime.rs 复用了这两个客户端。

concepts/src/link.rs
pub struct SocketConnector;
pub enum SocketTarget {
Tcp(SocketAddr),
Udp {
ipv6: bool,
},
}
impl Connector<SocketTarget> for SocketConnector {
type Stream = TcpStream;
type Datagram = UdpOutbound;
type Future =
Pin<Box<dyn Future<Output = io::Result<Outbound<TcpStream, UdpOutbound>>> + Send>>;
}

SocketConnector 是不依赖其他组件的参考 connector:

  • SocketTarget::Tcp(addr) 执行 TcpStream::connect(addr)。
  • SocketTarget::Udp { ipv6 } 在 [::]:0 或 0.0.0.0:0 上绑定一个未连接的 socket,并把它包装成 UdpOutbound。该 socket 从不调用 connect,因此 Effect::SendTo 按数据包选择对端。绑定时只固定地址族。

在所固定的版本中,workspace 和 katana 都没有构造 SocketConnector。同一约定的策略感知实现是 environment/src/dial/mod.rs → Dialer,它以相同的 Stream 和 Datagram 类型实现了 Connector<SocketTarget>:

SocketConnector 作为 Connector<SocketTarget> 的 Dialer
TCP TcpStream::connect(addr),自身没有超时 TcpDialer::connect(addr),带 SocketOptions 和连接超时(DEFAULT_CONNECT_TIMEOUT,10 秒)
UDP 在未指定地址上执行 UdpSocket::bind UdpDialer::bind(AddressFamily::V4) 或 V6,带 SocketOptions;IPv6 socket 为 v6-only

Dialer 还实现了 Connector<DialTarget>:它按顺序尝试一组 TCP 地址,或者为每个地址族各绑定一个 socket,组成 DualStackUdp。两者都在拨号器页面中介绍。

concepts/src/link.rs
pub enum NoStream {}
pub enum NoDatagram {
Never(Infallible),
}
impl AsyncRead for NoStream { /* match *self {} */ }
impl AsyncWrite for NoStream { /* match *self {} */ }
impl DatagramLink for NoDatagram {
type Addr = Destination;
// match *self { NoDatagram::Never(never) => match never {} }
}

它们是从不产出对应类型的 connector 所使用的 Stream 和 Datagram 类型。两个类型都无法持有值:NoStream 没有变体,NoDatagram 唯一的变体包装了一个 Infallible。它们的 trait 方法都是空 match,编译器之所以接受,是因为不可能有值到达那里。Outbound<TcpStream, NoDatagram> 只可能是 Stream,因此运行时无需检查。

NoDatagram 有意声明 Addr = Destination。这样它就满足运行时的 Conn::Datagram: DatagramLink<Addr = Destination> 约束,使仅支持流的 connector 也能驱动 ProxyServerRuntime。

类型 生产环境使用者 测试使用者
NoDatagram TransportConnector:代理的 UDP 承载在其流内部,Udp 目的地会被拒绝,文本为 a proxy transport carries no datagrams of its own concepts/tests/runtime.rs 和 concepts/tests/client.rs 中的 duplex 拨号器;protocols/tests/support/pipeline.rs → TcpConnector;katana 的 tests/unit/serve.rs 和 tests/unit/e2e.rs
NoStream 无 concepts/tests/runtime.rs 中类型为 Outbound<NoStream, UdpOutbound> 的 UDP connector

codec 层面的对应物是 concepts/src/core.rs → NoCodec<Target, Error>,见客户端运行时页面。

concepts/src/net.rs
pub enum UserAuthorization {
UsernamePassword {
username: CompactString,
password: CompactString,
},
Uuid(Uuid),
}

UserAuthorization 是用户被匹配时所依据的身份。它实现了 Clone、PartialEq 和 Eq,但没有实现 Hash。协议特有的密钥材料(例如派生密钥或加密状态)应放在 NetworkUser::user_data 中,而不是这里。etemenanki-protocols 中的每个服务端协议核心都会在用户认证通过后构建它,且都不在其中存放秘密:

协议核心 变体 username password
带账户的 SOCKS5(socks/handshake.rs) UsernamePassword 匹配到的账户名 空
不带账户的 SOCKS5、SOCKS4 UsernamePassword 空 空
HTTP(http/core.rs) UsernamePassword 匹配到的账户名;入站没有账户时为空 空
Trojan、Shadowsocks、Shadowsocks 2022 多用户 UsernamePassword 匹配到的用户的 email 空
Shadowsocks 2022 单密钥、TUN UsernamePassword 空 空
Hysteria 2 UsernamePassword 已认证用户的 label 空
VLESS、VMess Uuid

katana 以这个值作为其按用户注册表的键。由于该类型没有实现 Hash,katana 在 src/traffic.rs 中把它投影为自己的 AuthKey(Uuid 或 Name)。

concepts/src/net.rs
pub struct NetworkUser<T> {
pub authorization: UserAuthorization,
pub user_data: std::sync::Arc<T>,
}
impl<T> Clone for NetworkUser<T> {
fn clone(&self) -> Self;
}

一个已认证用户由它的授权信息加上位于 Arc 之后、任意的按用户载荷 T 组成。载荷由构建入站用户表的一方设置,该用户打开的每条流共享同一个实例:

使用方 T
etemenanki-app(app/src/flow.rs:Flow = etemenanki_protocols::flow::Flow<()>) ()
katana(src/connector.rs:Connector<Flow<UserTag>>) UserTag,即用户的 AuthKey 和面板 uid;KatanaConnector::connect 为每条流据此查找该用户的计数器

Clone 是手写的,而不是 derive 的。#[derive(Clone)] 会加上 T: Clone 约束,但载荷只存在于 Arc 之后,而 Arc<T> 对任何 T 都可以克隆。因此,必须保持为单一共享实例的载荷可以是非 Clone 类型,而用户本身仍然可以克隆。处理流水线会频繁克隆用户:

  • Flow::toward 用承载流的用户和来源构建一条指向新目的地的子流,为此它会克隆用户。mux.cool 子流(protocols/src/mux/demux.rs)、Trojan UDP 关联为第一个数据包打开的流,以及 app 的逐包 UDP 路由(app/src/outbound/udp_fanout.rs)都经过它。
  • SOCKS 服务端在转发来自其客户端的第一个数据报时,把用户克隆到它为 UDP 关联拨号的那一个 Flow 中。该数据报的目标成为这个流的 destination。Shadowsocks 2022 和 VMess 协议核心则把它克隆到每个请求的 Flow 中。

Flow<T> 出于同样的原因手写了自己的 Clone,protocols/src/hysteria/server/authenticator.rs 中的 UserEntry<T> 也是如此。

concepts/src/net.rs
#[repr(u8)]
pub enum DialNetwork {
Unknown = 0,
Tcp = 1,
Udp = 2,
Unix = 3,
}

DialNetwork 是 Destination 的传输网络。它实现了 Copy、Eq 和 Hash。协议核心只会构建 Tcp 和 Udp,所有使用方也都按这两个值分支:

  • AppConnector、KatanaConnector 和 FreedomConnector 对 Udp 走数据报路径。
  • TransportConnector 以 ErrorKind::Unsupported 拒绝 Udp。
  • app/src/router.rs 和 katana 的 src/router.rs 中的 route_target 都把 Tcp 和 Udp 映射为路由模型的 TargetNetwork,对其他值则不设置网络。
concepts/src/net.rs
pub enum Remote {
IpAddr(std::net::IpAddr),
Domain(CompactString),
}

Remote 是一个不一定已解析的主机。从线路上读到的域名会一直保持为域名,直到到达必须去连接它的组件。这样路由就可以按名称匹配它,再由所选出站的解析器和地址族策略决定如何到达它。没有任何服务端协议核心会解析域名。

路由模型在 environment/src/routing.rs 中有自己的借用型 Remote<'a>。route_target 负责二者之间的转换,借用域名字符串而不是复制它。

concepts/src/net.rs
pub struct Destination {
pub network: DialNetwork,
pub remote: Remote,
pub port: u16,
}
impl Destination {
pub fn udp(addr: std::net::SocketAddr) -> Self;
pub fn socket_addr(&self) -> Option<std::net::SocketAddr>;
}

Destination 是请求所指明的目标。它也是经由出站的数据报的逐包地址:Effect::SendTo、Effect::SendToHeld、Event::Datagram 和 Event::SendFailed 都携带一个 Destination。每个承载 UDP 的协议都会为每个数据包在线路上写入一个可能是域名的主机,因此协议核心原样传递该主机,由 link 决定如何到达它。Destination 实现了 Clone、Eq 和 Hash。

辅助方法 行为
Destination::udp(addr) network: DialNetwork::Udp、remote: Remote::IpAddr(addr.ip())、port: addr.port()。普通 socket link 用它报告收到的数据包的来源。
destination.socket_addr() 对 Remote::IpAddr 返回 Some(SocketAddr),对 Remote::Domain 返回 None。它忽略 network。普通 UDP 出站用它来拒绝域名。
concepts/src/sniff.rs
pub enum SniffedProtocol {
Tls,
Http,
}
pub struct SniffedBehavior {
pub protocol: SniffedProtocol,
pub domain: CompactString,
}
pub trait Sniffer {
fn sniff(&self, data: &[u8]) -> Option<SniffedBehavior>;
}

Sniffer 检查一条流开头的载荷字节,可能从中恢复出代理请求头没有携带的路由线索:TLS SNI 或 HTTP Host。sniff 接收 &self 和一个普通切片并返回 Option,因此嗅探器没有办法让连接失败。格式错误、被截断或无法识别的输入会得到 None,流照常按其目的地路由。

concepts crate 只定义这些形状,其余部分由 protocols/src/sniff/ 提供:

  • TlsSniffer 和 HttpSniffer,两个 Sniffer 实现;
  • 函数 sniff(data),它先尝试 TLS,因为 TLS 记录头的区分度更高;
  • plausible_domain,拒绝 IP 字面量、长于 253 字节的名称,以及 [A-Za-z0-9._-] 之外的字符;
  • worth_sniffing(destination),只对 Remote::IpAddr 目的地返回 true;
  • 预算 SNIFF_TIMEOUT(300 毫秒)和 SNIFF_LIMIT(4 KiB,跨帧累计)。

结果放在 Flow::sniffed 中传递。入站在拨号之前设置它,并且只在该入站启用了嗅探、且 worth_sniffing 允许时才设置。路由只读取 domain:两个 route_target 函数都把它传给 RouteTarget::with_sniffed_domain,之后每个域名匹配条件除了请求自身的主机外也会尝试它。protocol 会被携带,但没有使用方读取它。收集器以及各协议的“暂留后打开”逻辑见嗅探页面。

relay.rs 为两端之间没有协议的路径提供两个手写 future。每个方向持有一个固定缓冲区,在构造时由 boxed_array 在堆上分配。不为每个方向创建 task,不使用 channel,new 之后也不再分配内存。两个 future 自身都没有超时;需要超时的调用方自行包装 future。

concepts/src/relay.rs
pub struct UnidirectionalConnection<I, O, const BUF_SIZE: usize = 8192> { /* … */ }
impl<I, O, const BUF_SIZE: usize> UnidirectionalConnection<I, O, BUF_SIZE> {
pub fn new(input: I, output: O) -> Self;
pub fn transferred(&self) -> u64;
}
impl<I: AsyncRead, O: AsyncWrite, const BUF_SIZE: usize> Future
for UnidirectionalConnection<I, O, BUF_SIZE>
{
type Output = io::Result<u64>;
}

UnidirectionalConnection 把 input 拷贝到 output,直到 input 到达流结束,然后 flush output,并以字节数 resolve。input 和 output 通过 #[pin_project] 做结构化 pin,因此二者都不必是 Unpin。到达流结束时它会 flush,但不会关闭 output。在所固定的版本中,workspace 和 katana 中都没有代码使用它。

concepts/src/relay.rs
pub struct Relayed {
pub a_to_b: u64,
pub b_to_a: u64,
}
pub struct BidirectionalConnection<A, B, const BUF_SIZE: usize = 8192> { /* … */ }
impl<A, B, const BUF_SIZE: usize> BidirectionalConnection<A, B, BUF_SIZE> {
pub fn new(a: A, b: B) -> Self;
pub fn relayed(&self) -> Relayed;
}
impl<A, B, const BUF_SIZE: usize> Future for BidirectionalConnection<A, B, BUF_SIZE>
where
A: AsyncRead + AsyncWrite + Unpin,
B: AsyncRead + AsyncWrite + Unpin,
{
type Output = io::Result<Relayed>;
}

BidirectionalConnection 在一个 future 中同时拷贝 a → b 和 b → a。每个方向是一个私有的 Half<BUF_SIZE>,拥有自己的缓冲区、游标(pos、cap)、标志(read_done、shutdown_done、need_flush)和字节计数。每次 poll 都用同一个 Context 推进两个 half,把同一个端点借给一个 half 作为读端、借给另一个 half 作为写端。因此 socket 不需要 split。端点没有被 pin,这就是 Future impl 要求两者都是 Unpin 的原因。future 在两个 half 都完成后才 resolve。Relayed 实现了 Copy 和 PartialEq,relayed() 可以随时读取,调用方正是借此检测空闲的中继。

单个 half 在以下状态之间转换:

stateDiagram-v2
  [*] --> Fill
  Fill --> Drain: 读到字节
  Fill --> Parked: 读返回 Pending(若 need_flush 则先 flush)
  Parked --> Fill: 被唤醒
  Fill --> ShutDown: 读到 0 字节
  Drain --> Drain: 部分写入
  Drain --> Fill: 缓冲区已写空
  ShutDown --> Done: poll_shutdown 就绪
  Fill --> Failed: 读错误
  Drain --> Failed: 写错误或写入 0 字节
  ShutDown --> Failed: shutdown 错误
  Done --> [*]
  Failed --> [*]
  • Parked。 half 在空闲读端上返回 Pending 之前,如果自上次 flush 以来写过数据(need_flush),会先 flush 写端。因此,即使不再有输入到达,缓冲在 TLS 或 WebSocket 写端中的字节也能到达对端。
  • ShutDown。 到达流结束时,half 对其写端调用 poll_shutdown:即半关闭。另一个方向继续流动。UnidirectionalConnection 在这一点改为执行 flush。
  • Failed。 读错误、写错误,或写入返回 0(ErrorKind::WriteZero),都会让整个 future 以该错误结束。a → b half 之后的 ? 会在本次调用中 poll b → a half 之前就返回。

协议核心运行在服务端运行时内部,不使用 relay.rs。在 596916d,它唯一的调用方是 protocols/src/socks/server.rs 中的 SOCKS CONNECT 路径。SOCKS 是唯一一个服务端无法套进 ProxyCoreDecode 的入站,因为它的 UDP 一侧位于第二个 socket 上。完成握手和 connector 拨号后,CONNECT 路径通过以下函数中继客户端流与 connector 返回的流:

protocols/src/socks/server.rs
async fn relay_with_idle_guard<A, B>(a: A, b: B) -> io::Result<Relayed>
where
A: AsyncRead + AsyncWrite + Unpin,
B: AsyncRead + AsyncWrite + Unpin,

它构建一个 BidirectionalConnection::<A, B, RELAY_BUF>(每个方向 16 KiB),并在循环中让它与一个新的 RELAY_IDLE_TIMEOUT(300 秒)sleep 竞争。sleep 触发时,它把 relayed() 与上一次快照比较。如果没有任何字节流动,就返回 ErrorKind::TimedOut,文本为 socks: relay idle;否则保存新快照并再次 sleep。因此,一个陷入静默的中继会在一到两个 RELAY_IDLE_TIMEOUT 周期之后被关闭。返回时 future 被 drop,进而 drop 两个端点并关闭两条连接。

UDP ASSOCIATE 路径同样不使用 relay.rs。它在任何运行时之外,用自己基于 hub socket 的 select! 循环驱动 connector 的 DatagramLink,并由 protocols/src/socks/server.rs 中的 ExpectedSender 决定哪些 hub 数据报能到达该链路:

  • admits(from) 要求来源是控制连接的 IP;若控制连接走 Unix socket,则要求是请求中指定的 IP。它使用 protocols/src/socks/protocol.rs 中的 endpoint 进行比较,该函数把 IP 转为规范形式,因此 IPv4 映射的 IPv6 发送方就等同于对应的 IPv4 发送方。一旦端口被固定(由请求指定或由 pin 固定),它还要求端口一致。未通过检查的数据报在解析之前就被丢弃。
  • 对每个通过准入、能够解析且载荷非空的数据报,pin(from) 都会在拨号链路和发送载荷之前运行。第一次 pin 的结果保持不变,因此第一个这样的数据报确定了客户端,之后 client() 给出的就是链路回复发往的唯一地址。
  • 循环为同一个数据报拨号该关联唯一的链路。因此,来自任何其他发送方的数据报既不会打开出站,也永远收不到回复。

SOCKS 页面介绍了请求可以预先固定的端口、Unix socket 的情况,以及中继无法将关联绑定到其客户端时返回的 0x02 拒绝。

客户端一侧的链路使用同样的比较。protocols/src/socks/udp_link.rs 中的 SocksUdpLink::poll_recv_from 会跳过所有 endpoint 与中继不同的数据报,以及无法解析的数据报。因此,socket 以双栈方式绑定在 [::] 上的链路仍能收到 IPv4 中继的数据报,该 socket 会以 IPv4 映射形式报告中继地址。

katana 没有 SOCKS 入站,也不引用 relay.rs。

Section titled “数据流:一个 UDP 数据包经过出站 link”

数据报路径最能清楚地体现 link 约定。协议核心、运行时、connector 和 link 都在同一个 task 中运行,下图中的每个箭头都是一次函数调用或一次 poll,而不是 channel。

sequenceDiagram
  participant C as ProxyCoreDecode
  participant R as ProxyServerRuntime
  participant K as Connector
  participant L as DatagramLink (Addr = Destination)
  C->>R: Effect::Open (key, target: Flow)
  R->>K: connect(target),future 被 box 后存入槽位
  K-->>R: Ok(Outbound::Datagram(link))
  R->>C: Event::Connected (key)
  C->>R: Effect::SendTo (key, to: Destination, range)
  R->>L: poll_send_to(bytes, to)
  alt Ready(Ok(n))
    R->>R: outbound_tx += n
  else Ready(Err(e)),例如普通 link 上的域名
    R->>C: Event::SendFailed (key, to, error),key 保持存活
  end
  L-->>R: poll_recv_from 返回 Ok(from)
  R->>C: Event::Datagram (key, from, data)
  L-->>R: poll_recv_from 返回 Err(e)
  R->>C: Event::OutboundError (key, error),key 被移除

流出站遵循相同的形态,只是换成 Effect::Forward 和 Effect::Shutdown、AsyncRead 和 AsyncWrite 的 poll 方法,以及 Event::Outbound 和 Event::OutboundEof。在流出站上,任何读写错误都会移除该 key。连接生命周期页面端到端地追踪一条完整连接。

不变量 机制 由谁固定
存活的出站可以被移动。 DatagramLink: Unpin 和 Connector::Stream: Unpin。运行时把槽位按值保存在 BTreeMap 中。 编译器
服务端运行时只发送以 Destination 寻址的数据报。 运行时 impl 上的 Conn::Datagram: DatagramLink<Addr = Destination>。NoDatagram 声明了该 Addr,使仅支持流的 connector 也满足条件。 编译器
普通 UDP 出站从不解析域名。 UdpOutbound 和 DualStackUdp 调用 socket_addr(),在得到 None 时返回 ErrorKind::Unsupported。 没有测试通过这两个 link 发送域名。它们共用的拒绝路径由 concepts/tests/runtime.rs 中的 a_refused_datagram_send_keeps_the_key_alive 固定,该测试通过 IPv4 socket 向 IPv6 对端发送。
发送数据报被拒绝时保留 key;接收失败时移除 key。 concepts/src/runtime.rs 中的 report_send_failure 不动槽位;fail_outbound 调用 forget_key。 concepts/tests/runtime.rs 中的 a_refused_datagram_send_keeps_the_key_alive
发往数据报传输层对端的数据包被拒绝不是致命错误。 数据报传输层上的发送错误变为 Event::TransportSendFailed。 concepts/tests/runtime.rs 中的 a_refused_transport_packet_is_reported_not_fatal 和 single_peer_datagram_transport_demuxes_sessions_and_reports_refusals
拨号失败以事件而不是运行时错误的形式到达协议核心。 box 后的拨号 future 返回 Poll::Ready(Err(error)) → forget_key → Event::ConnectFailed。 concepts/tests/runtime.rs 中的 connect_failure_reaches_the_core_as_an_event
流 effect 永远不会落到数据报出站上,反之亦然。 effect 循环中 LinkState 的 match 分支返回 RuntimeError::WrongLinkKind。 没有专门的测试
客户端运行时的线路总是流。 ProxyClientRuntime::wire 把 Outbound::Datagram 映射为 ErrorKind::Unsupported,并设置 Wire::Down。 没有专门的测试
克隆用户从不要求 T: Clone。 手写的 impl<T> Clone for NetworkUser<T> 和 impl<T> Clone for Flow<T> 克隆的是 Arc。 对所有实际使用的载荷类型由编译器保证;没有测试使用非 Clone 载荷
来自线路的域名在到达路由时仍未解析。 Remote::Domain 随 Destination 携带,route_target 把它映射为 routing::Remote::Domain。 在协议核心一侧,protocols/tests/unit/http/core.rs 中的 connect_to_a_domain_answers_200_once_connected 检查打开的 Flow 指向该域名。没有测试固定 route_target 转换本身。
嗅探到的域名让域名规则可以匹配以 IP 寻址的流。 Flow::sniffed,由 route_target 通过 RouteTarget::with_sniffed_domain 读取 app/tests/integration/e2e_sniff.rs 中的 an_http_host_routes_an_ip_addressed_flow、a_tls_sni_routes_an_ip_addressed_flow 和 turning_sniffing_off_stops_the_domain_rule_matching;environment/tests/unit/routing.rs 中的 a_sniffed_domain_makes_an_ip_target_match_domain_rules
中继分别半关闭每个方向,并报告准确的计数。 Half::poll 在流结束时调用 poll_shutdown,future 只在两个 half 都 Ready 时才 resolve。 concepts/src/relay.rs 中的 bidirectional_relays_both_ways_and_half_closes,使用 16 字节缓冲区和 41 字节消息,使拷贝绕回缓冲区
输入静默时,缓冲的中继输出不会滞留。 half 在读端返回 Pending 之前先 flush need_flush。 没有专门的测试
SOCKS UDP 关联只接收来自其自身客户端的数据报送入链路,也只回复该客户端。 ExpectedSender::admits 在解析数据包之前运行;pin 在拨号链路之前运行;回复发往 client()。 protocols/tests/unit/socks/server.rs 中的 only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port 和 an_ipv4_mapped_address_is_the_ipv4_one;protocols/tests/pipeline/socks.rs 中的 udp_association_ignores_another_ip 和 udp_association_ignores_another_port_once_pinned
无论 socket 以何种形式报告中继地址,SocksUdpLink 只返回中继的数据报。 poll_recv_from 比较 endpoint(from) 与 endpoint(self.relay),并跳过不匹配的数据报。 protocols/tests/pipeline/socks.rs 中的 udp_link_ignores_datagrams_not_from_the_relay 和 udp_link_on_a_dual_stack_socket_hears_an_ipv4_relay;protocols/tests/unit/socks/protocol.rs 中的 endpoint_sees_through_ipv4_mapping_and_ignores_flow_info
位置 失败 结果
Connector::connect 的 future Err(e) 服务端运行时:遗忘该 key,协议核心收到 Event::ConnectFailed。客户端运行时:Wire::Down,第一次明文调用以 e 失败,之后每次调用都以 NotConnected 失败。SOCKS CONNECT:发送拒绝应答(ConnectionRefused 对应 SOCKS5 代码 0x05,其他情况为 0x04)。如果嗅探已经接受了请求并收集了一段客户端字节前缀,则不写拒绝应答,客户端看到的是连接关闭。
DatagramLink::poll_send_to,出站 Err(e) 丢弃该数据包,协议核心收到 Event::SendFailed,key 保留。
DatagramLink::poll_recv_from,出站 Err(e) 移除该 key,协议核心收到 Event::OutboundError。一旦上游结束了该流,ProxyClientRuntime 的数据报 link 在这里返回 UnexpectedEof。
DatagramLink,传输层 发送 Err / 接收 Err Event::TransportSendFailed / RuntimeError::Transport,后者结束运行时。
Outbound 变体不符 见 Outbound<S, D> 下的表格 RuntimeError::WrongLinkKind,或来自使用方的 ErrorKind::Unsupported。
BidirectionalConnection 任一 half 中的读、写或 shutdown 错误,或 WriteZero future resolve 为 Err。调用方 drop 它,进而 drop 两个端点。
SOCKS 中继 在一个完整的 RELAY_IDLE_TIMEOUT 检查间隔内两个方向都没有字节 ErrorKind::TimedOut,socks: relay idle。

取消就是 drop。本页的每个类型都归 poll 它的 future 所有。drop 一个 ProxyServerRuntime 会 drop 仍在进行中的拨号 future 以及其 map 中的每个 link。drop 一个中继 future 会 drop 它拥有的端点。这些类型都不 spawn task,因此它们拥有的任何东西都不会比连接活得更久。

常量 值 定义位置 作用于
两个中继 future 的 BUF_SIZE 默认值 每个方向 8192 字节 concepts/src/relay.rs UnidirectionalConnection、BidirectionalConnection
RELAY_BUF 每个方向 16 * 1024 字节 protocols/src/socks/server.rs SOCKS CONNECT 中继
RELAY_IDLE_TIMEOUT 300 秒 protocols/src/core/mod.rs SOCKS 中继的空闲守卫,以及各协议核心的空闲截止时间
ProxyCoreDecode::MAX_DATAGRAM 默认 4096 字节,可按协议核心覆盖 concepts/src/core.rs 服务端运行时从 DatagramLink 一次完整读取的最大数据包;对出站上限为 BUF_SIZE - STAGING_RESERVE,对传输层上限为 BUF_SIZE
DEFAULT_CONNECT_TIMEOUT 10 秒 environment/src/dial/tcp.rs Dialer 的 TCP 连接,除非被 TcpDialer::with_connect_timeout 覆盖;SocketConnector 没有该超时
SNIFF_TIMEOUT 300 毫秒 protocols/src/sniff/mod.rs 入站等待第一段载荷的时长,超时后不带嗅探域名直接路由
SNIFF_LIMIT 4 * 1024 字节 protocols/src/sniff/mod.rs 嗅探器可以检查的字节数,跨帧累计
测试 文件 固定的行为
bidirectional_relays_both_ways_and_half_closes concepts/src/relay.rs 双向拷贝、半关闭,以及在缓冲区小于消息时得到 Relayed { a_to_b: 41, b_to_a: 5 }
datagrams_round_trip_through_a_real_udp_socket concepts/tests/runtime.rs 在流传输层下,返回 Outbound::<NoStream, UdpOutbound>::Datagram 的闭包 connector,以及字节计数
a_refused_datagram_send_keeps_the_key_alive concepts/tests/runtime.rs poll_send_to 错误变为 SendFailed,同一个 key 之后仍能中继
datagram_outbound_is_never_truncated_by_staging_backpressure concepts/tests/runtime.rs 只有在有空间容纳一个完整数据包时才读取数据报出站
connect_failure_reaches_the_core_as_an_event concepts/tests/runtime.rs 闭包 connector 的 Err 变为 ConnectFailed
datagram_transport_demultiplexes_peers_and_frames_replies concepts/tests/runtime.rs 裸 UdpSocket(Addr = SocketAddr)作为数据报传输层,以函数 udp_connector 作为 connector
a_refused_transport_packet_is_reported_not_fatal concepts/tests/runtime.rs 传输层一侧的发送错误变为 TransportSendFailed
single_peer_datagram_transport_demuxes_sessions_and_reports_refusals concepts/tests/runtime.rs Addr = () 的 link(TinyQuic)作为传输层
dial_failure_surfaces_on_first_use concepts/tests/client.rs 闭包 connector 的拨号错误到达客户端运行时的第一次调用,之后的调用返回 NotConnected
socket_target_connector_binds_one_family environment/tests/integration/udp.rs 作为 Connector<SocketTarget> 的 Dialer 绑定一个 IPv4 socket 并产出 UdpOutbound
dual_stack_link_serves_a_proxy_runtime environment/tests/integration/udp.rs Dialer 直接作为服务端运行时的 connector,产出双向中继一个数据包的 DualStackUdp
transport_connector_refuses_udp protocols/tests/pipeline/transports.rs connect 所包装的 TransportConnector::dial 以 ErrorKind::Unsupported 拒绝 Udp 目的地
new_server_vs_new_client_tcp、new_server_vs_new_client_udp protocols/tests/pipeline/socks.rs 端到端的 SOCKS CONNECT 中继,以及作为 DatagramLink<Addr = Destination> 的 SocksUdpLink
udp_association_ignores_another_ip(仅 Linux)、udp_association_ignores_another_port_once_pinned protocols/tests/pipeline/socks.rs 来自其他 IP 的数据报,或客户端固定后来自其他端口的数据报,都不会被转发,且其他 IP 上的发送方收不到回复
only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port、an_ipv4_mapped_address_is_the_ipv4_one protocols/tests/unit/socks/server.rs ExpectedSender:只接受控制连接对端的 IP,第一次 pin 保持不变,IPv4 映射的发送方与其 IPv4 对端匹配
udp_link_ignores_datagrams_not_from_the_relay protocols/tests/pipeline/socks.rs SocksUdpLink::poll_recv_from 跳过入侵者的数据报,返回中继的数据报
udp_link_on_a_dual_stack_socket_hears_an_ipv4_relay(仅 Linux) protocols/tests/pipeline/socks.rs 绑定在 [::] socket 上的 SocksUdpLink 能通过 IPv4 中继完成中继
endpoint_sees_through_ipv4_mapping_and_ignores_flow_info protocols/tests/unit/socks/protocol.rs endpoint 把 IPv4 映射地址与 IPv4 地址视为相同,并忽略 IPv6 flow info
extracts_sni_from_a_real_client_hello、a_truncated_client_hello_yields_nothing_rather_than_garbage、an_ip_literal_sni_is_rejected、non_tls_and_malformed_input_yield_nothing protocols/tests/unit/sniff/tls.rs 作为 Sniffer 的 TlsSniffer
extracts_the_host_header、an_ip_host_is_rejected、bytes_that_merely_contain_a_host_header_are_not_http protocols/tests/unit/sniff/http.rs 作为 Sniffer 的 HttpSniffer
domain_plausibility protocols/tests/unit/sniff/mod.rs plausible_domain,每个嗅探到的名称都要经过的过滤器

UnidirectionalConnection 和 SocketConnector 既没有测试,也没有调用方。