Link、connector 与网络类型
源码文件:50 个 · 核对版本 Etemenanki 596916d · katana v3.0.1
Etemenanki/concepts/src/link.rsEtemenanki/concepts/src/net.rsEtemenanki/concepts/src/sniff.rsEtemenanki/concepts/src/relay.rsEtemenanki/concepts/src/core.rsEtemenanki/concepts/src/runtime.rsEtemenanki/concepts/src/client.rsEtemenanki/concepts/tests/runtime.rsEtemenanki/concepts/tests/client.rsEtemenanki/environment/src/dial/mod.rsEtemenanki/environment/src/dial/tcp.rsEtemenanki/environment/src/dial/udp.rsEtemenanki/environment/src/routing.rsEtemenanki/environment/tests/integration/udp.rsEtemenanki/protocols/src/flow.rsEtemenanki/protocols/src/core/mod.rsEtemenanki/protocols/src/sniff/mod.rsEtemenanki/protocols/src/socks/handshake.rsEtemenanki/protocols/src/socks/server.rsEtemenanki/protocols/src/socks/protocol.rsEtemenanki/protocols/src/socks/udp_link.rsEtemenanki/protocols/src/http/core.rsEtemenanki/protocols/src/trojan/core.rsEtemenanki/protocols/src/ss_legacy/core.rsEtemenanki/protocols/src/ss_2022/core.rsEtemenanki/protocols/src/vless/core.rsEtemenanki/protocols/src/vmess/core.rsEtemenanki/protocols/src/hysteria/server/inbound.rsEtemenanki/protocols/src/tun/inbound.rsEtemenanki/protocols/src/transports/connect.rsEtemenanki/app/src/connector.rsEtemenanki/app/src/router.rsEtemenanki/app/src/outbound/freedom.rsEtemenanki/app/src/outbound/proxy.rsEtemenanki/app/src/outbound/udp_fanout.rsEtemenanki/app/src/flow.rsEtemenanki/protocols/src/mux/demux.rsEtemenanki/protocols/src/hysteria/server/authenticator.rsEtemenanki/protocols/tests/pipeline/transports.rsEtemenanki/protocols/tests/pipeline/socks.rsEtemenanki/protocols/tests/unit/socks/server.rsEtemenanki/protocols/tests/unit/socks/protocol.rsEtemenanki/app/tests/integration/e2e_sniff.rskatana/src/connector.rskatana/src/router.rskatana/src/traffic.rskatana/src/outbound/proxy.rskatana/tests/unit/serve.rskatana/tests/unit/e2e.rskatana/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,因此它正是网络类型与嗅探类型的交汇点:
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>;各部分如何组合
Section titled “各部分如何组合”服务端协议核心决定打开什么。运行时请 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,以及任意闭包 |
任意 | 任意 |
DatagramLink
Section titled “DatagramLink”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 测试使用) |
运行时中这两个约束的写法如下:
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>>;}每种结果对运行时意味着什么
Section titled “每种结果对运行时意味着什么”在两种角色下,运行时对 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 则把如何处理交给协议核心决定。
UdpSocket 的实现
Section titled “UdpSocket 的实现”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。
UdpOutbound
Section titled “UdpOutbound”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 出站页面。
Outbound<S, D>
Section titled “Outbound<S, D>”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。
Connector
Section titled “Connector”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返回具名 futureProxyClientConnecting,它只在上游拨通且 codec 握手完成后才 resolve。- future 的错误就是该出站的失败。服务端运行时会移除该 key,丢弃仍为它排队的所有 effect(
forget_key),并投递Event::ConnectFailed { key, error }。
针对闭包的 blanket impl
Section titled “针对闭包的 blanket impl”impl<Target, F, Fut, S, D> Connector<Target> for Fwhere 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 测试使用闭包和普通函数:
fn udp_connector(_: ()) -> Ready<io::Result<Outbound<NoStream, UdpOutbound>>>fn dialer(dials: Dials) -> impl FnMut(String) -> DialFutureconcepts/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 复用了这两个客户端。
SocketConnector 与 SocketTarget
Section titled “SocketConnector 与 SocketTarget”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。两者都在拨号器页面中介绍。
NoStream 与 NoDatagram
Section titled “NoStream 与 NoDatagram”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>,见客户端运行时页面。
UserAuthorization
Section titled “UserAuthorization”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)。
NetworkUser<T> 及其手写的 Clone
Section titled “NetworkUser<T> 及其手写的 Clone”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> 也是如此。
DialNetwork
Section titled “DialNetwork”#[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,对其他值则不设置网络。
Remote
Section titled “Remote”pub enum Remote { IpAddr(std::net::IpAddr), Domain(CompactString),}Remote 是一个不一定已解析的主机。从线路上读到的域名会一直保持为域名,直到到达必须去连接它的组件。这样路由就可以按名称匹配它,再由所选出站的解析器和地址族策略决定如何到达它。没有任何服务端协议核心会解析域名。
路由模型在 environment/src/routing.rs 中有自己的借用型 Remote<'a>。route_target 负责二者之间的转换,借用域名字符串而不是复制它。
Destination
Section titled “Destination”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 出站用它来拒绝域名。 |
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 会被携带,但没有使用方读取它。收集器以及各协议的“暂留后打开”逻辑见嗅探页面。
中继 future
Section titled “中继 future”relay.rs 为两端之间没有协议的路径提供两个手写 future。每个方向持有一个固定缓冲区,在构造时由 boxed_array 在堆上分配。不为每个方向创建 task,不使用 channel,new 之后也不再分配内存。两个 future 自身都没有超时;需要超时的调用方自行包装 future。
UnidirectionalConnection
Section titled “UnidirectionalConnection”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 中都没有代码使用它。
BidirectionalConnection 与 Relayed
Section titled “BidirectionalConnection 与 Relayed”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 → bhalf 之后的?会在本次调用中 pollb → ahalf 之前就返回。
中继仍在何处使用
Section titled “中继仍在何处使用”协议核心运行在服务端运行时内部,不使用 relay.rs。在 596916d,它唯一的调用方是 protocols/src/socks/server.rs 中的 SOCKS CONNECT 路径。SOCKS 是唯一一个服务端无法套进 ProxyCoreDecode 的入站,因为它的 UDP 一侧位于第二个 socket 上。完成握手和 connector 拨号后,CONNECT 路径通过以下函数中继客户端流与 connector 返回的流:
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。
数据流:一个 UDP 数据包经过出站 link
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 |
失败路径与取消
Section titled “失败路径与取消”| 位置 | 失败 | 结果 |
|---|---|---|
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 既没有测试,也没有调用方。