- 网络安全
- 运维
- 认证鉴权
【免费下载链接】warpgate
Fully transparent SSH, HTTPS, Kubernetes, database and RDP/VNC bastion/PAM that doesn't need additional client-side software
导读
Warpgate 是一款无需任何客户端软件、完全透明的 SSH、HTTPS、Kubernetes、数据库及 RDP/VNC 堡垒机(bastion)/PAM 网关。为了让 RDP 协议栈适配自己的堡垒机场景,Warpgate 将上游 IronRDP 生态中的ironrdp-async(0.10.0)以 path dependency 方式 vendored 进仓库,并在其上打了两个关键补丁:一是为 CredSSP 握手增加"无凭据(credential-less)"模式,支撑 RDP 目标的interactive_logon交互登录能力;二是为Framed::read_by_hint增加零长度未匹配帧防护,避免恶意或畸形 RDP 数据流把读取器拖入 100% CPU 空转。本文以 vendor/ironrdp-async/PATCHES.md 为骨架,结合 vendor/ironrdp-async/warpgate.patch 及warpgate-protocol-rdp、ironrdp-connector等仓库源码,逐条还原每个补丁的背景、实现与运维要点。
读完本文,你将掌握:Warpgate 为什么必须 fork 上游 IronRDP 组件、credssp_credentialless配置如何贯穿ironrdp-connector → ironrdp-async → warpgate-protocol-rdp三层调用链、以及零长度帧防护补丁的具体 diff 与其回归测试。
背景:为什么 Warpgate 需要 vendored 一份ironrdp-async
Warpgate 的 RDP 代理(warpgate-protocol-rdp)在"网关侧扮演 RDP 客户端、连接真实目标主机"时,依赖 IronRDP 生态提供的异步封装ironrdp-async。该 crate 的定位正如其描述:为 IronRDP 状态机提供Future包装(见 vendor/ironrdp-async/Cargo.toml)。
上游 0.10.0 版本无法直接满足 Warpgate 的两个需求:
- CredSSP 无凭据模式:上游在
ironrdp-connector中硬编码了CredSspMode::WithCredentials,而 Warpgate 的交互登录目标恰恰需要相反行为——CredSSP 只做连接级认证(NLA),不向服务器委托任何凭据; Framed::read_by_hint空转缺陷:上游修复(backport 自上游)之前,一个长度为 0 且未匹配的帧会被read_by_hint无限"跳过",导致读取器在无任何 I/O 的情况下 100% 占用 CPU 空转(对应 Warpgate 的 issue warp-tech/warpgate#2585)。
因此仓库在 vendor/ironrdp-async/ 目录下完整保留了 fork 后的源码(src/connector.rs、src/framed.rs、src/session.rs、src/lib.rs)、补丁说明 PATCHES.md、可重放的源补丁 warpgate.patch,以及独立的Cargo.toml清单。
值得一提的是,这个 fork 与仓库中的另一个 vendored 组件 vendor/ironrdp-connector/ 是成对出现的:ironrdp-async是CredsspSequence::init的唯一调用方,而新增的参数恰恰由ironrdp-connectorfork 提供。PATCHES.md 明确提示:"Drop it together with the connector fork once upstream exposes the mode onConfig"——即一旦上游发布等价能力,两个 fork 应一并移除。
补丁一:Credential-less CredSSP(交互式登录)
问题:上游硬编码的WithCredentials
CredSSP(Credential Security Support Provider,用于 NLA——Network Level Authentication)握手完成后,客户端会在最后的 TSRequest 中携带TSPasswordCreds凭据。上游ironrdp-connector将 CredSSP 模式硬编码为CredSspMode::WithCredentials(见 vendor/ironrdp-connector/PATCHES.md),导致每条 NLA 连接都会委托TSPasswordCreds,服务器随后自动用这些凭据登录会话。
对于常规堡垒机场景这正合适:Warpgate 本身持有目标凭据(来自 SecretRef 或密码库),自动登录可以跳过服务器自己的登录界面。但 Warpgate 的interactive-logon目标选项需要完全相反的行为:连接仍然通过 CredSSP 完成认证,但不委托任何凭据,服务器由于拿不到可登录的凭据,只能展示自己的登录界面,由最终用户在浏览器/桌面客户端中手动输入目标主机凭据。
补丁实现:credssp_credentialless配置项
这个能力分两步落地:
第一步(ironrdp-connectorfork):新增Config::credssp_credentialless: bool字段(vendor/ironrdp-connector/src/lib.rs),并把它作为credentialless: bool参数传入CredsspSequence::init。init内部根据该参数在两种模式间切换(vendor/ironrdp-connector/src/credssp.rs):
let client = CredSspClient::new( server_public_key, credentials, if credentialless { credssp::CredSspMode::CredentialLess } else { credssp::CredSspMode::WithCredentials }, client_mode, service_principal_name, )CredSspMode::CredentialLess由 sspi 层实现——最终 TSRequest 中携带的是空的TSCredentials。模式选择逻辑参考 vendor/ironrdp-connector/warpgate.patch 中的 diff 上下文(CredSspMode::WithCredentials替换为条件选择)。
第二步(ironrdp-asyncfork):perform_credssp_step是CredsspSequence::init的唯一调用方(vendor/ironrdp-async/src/connector.rs),它把connector.config.credssp_credentialless原样透传:
let (mut sequence, mut ts_request) = CredsspSequence::init( connector.config.credentials.clone(), connector.config.domain.as_deref(), selected_protocol, server_name, server_public_key, kerberos_config, connector.config.credssp_credentialless, // ← fork 新增的唯一改动 )?;ironrdp-async这一侧的 patch 非常克制——PATCHES.md 明确指出"no other change",整个 diff 只在CredsspSequence::init调用中多传了一个参数(见 vendor/ironrdp-async/warpgate.patch)。设计上刻意只做透传、不做任何业务逻辑,目的就是让补丁可审计、可随上游演进干净地脱落。
调用链:从目标配置到 CredSSP 模式
要理解这个补丁的实战价值,需要追踪整条调用链。入口在 Warpgate 的 RDP 目标配置:TargetRdpOptions中的interactive_logon: bool字段(warpgate-common/src/config/target.rs),其文档注释准确描述了语义:
Show the target's own sign-in screen instead of logging on automatically. The stored credentials still pass network-level authentication (CredSSP).
即:存储的凭据仍用于 CredSSP 网络级认证,但不再用于自动登录——这正是"credential-less CredSSP"的精髓。
该配置在构建ironrdp_connector::Config时被翻译为两个联动开关(warpgate-protocol-rdp/src/client/mod.rs):
// Warpgate supplies the target credentials, so by default request autologon to // skip the server's own login UI (e.g. xrdp honours INFO_AUTOLOGON). An // interactive-logon target flips both switches: no autologon flag, and CredSSP // delegates no credentials — NLA still authenticates the connection, but the // server has nothing to log the session on with and shows its sign-in screen. autologon: !options.interactive_logon, credssp_credentialless: options.interactive_logon,两个开关必须联动,缺一不可:
autologon控制ClientInfoPdu中的INFO_AUTOLOGON标志(参考ironrdp-connector中该字段的文档,vendor/ironrdp-connector/src/lib.rs)。若只关闭credssp_credentialless而保留autologon,服务器可能仍尝试用空凭据自动登录;credssp_credentialless控制 CredSSP 委托阶段是否携带空TSCredentials。
对应地,会话侧的"登录状态"也随该选项切换:warpgate-protocol-rdp/src/lib.rs中connect()依据options.interactive_logon决定初始状态是LogonState::at_logon_screen()(停在登录界面)还是LogonState::logged_on()(已登录)(warpgate-protocol-rdp/src/lib.rs)。
完整调用链可归纳为:
TargetRdpOptions.interactive_logon → ironrdp_connector::Config { autologon, credssp_credentialless } (warpgate-protocol-rdp/src/client/mod.rs) → CredsspSequence::init(credentialless) (ironrdp-connector fork) → CredSspMode::CredentialLess / WithCredentials (ironrdp-connector/src/credssp.rs) → perform_credssp_step 透传参数 (ironrdp-async fork, vendor/ironrdp-async/src/connector.rs)移除策略
PATCHES.md 强调,一旦上游 IronRDP 发布包含等价Config开关的版本,ironrdp-async与ironrdp-connector两个 fork 应一并删除,回退到上游依赖。这为后续升级(Cargo.lock中的依赖 bump)提供了清晰的操作指引。
补丁二:Framed::read_by_hint零长度帧防护
问题:unmatched 零长度帧导致 100% CPU 空转
Framed是ironrdp-async提供的核心异步读取封装(vendor/ironrdp-async/src/framed.rs):它维护一个内部BytesMut缓冲区,通过peek()暴露缓冲内容,配合PduHint判断帧边界。read_by_hint的循环逻辑是(vendor/ironrdp-async/src/framed.rs):
pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result<Bytes> { loop { match hint.find_size(self.peek()).map_err(io::Error::other)? { Some((matched, length)) => { // ← 补丁在此处插入防护 let bytes = self.read_exact(length).await?.freeze(); if matched { return Ok(bytes); } else { debug!("Received and lost an unexpected PDU"); } } None => { let len = self.read().await?; // Handle EOF if len == 0 { return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); } } }; } }正常语义下,find_size返回(false, length)表示"缓冲区头部是一个不匹配的帧,读取并丢弃它后继续"。但当find_size返回(false, 0)——即未匹配且长度为 0——问题就出现了:代码会调用read_exact(0)。由于长度是 0,read_exact直接split_to(0)返回空缓冲区,不消耗任何字节、也不执行任何 I/O(vendor/ironrdp-async/src/framed.rs 中self.buf.len() >= length立即成立,self.read()根本不会执行)。于是循环无限空转:既不前进、也不 yield、永远观察不到 EOF,把单个 CPU 核钉死在 100%。
Warpgate 仓库中该缺陷被标记为 warp-tech/warpgate#2585。触发条件很现实:任何PduHint都可能对畸形头部报出(false, 0),例如带零长度字段的 fast-path 头字节。攻击者只需向网关发送这样的畸形数据,就能让 RDP 读取协程空转,造成单核 CPU 打满的拒绝服务。
补丁实现:在read_by_hint内统一拦截
防护逻辑被加在read_by_hint循环内部、read_exact之前(vendor/ironrdp-async/src/framed.rs):
// An unmatched zero-length frame can never be skipped past: `read_exact(0)` // consumes nothing and performs no I/O, so looping on it would spin without // yielding or ever observing EOF. Any `PduHint` can report `(false, 0)` on a // malformed header (e.g. a fast-path byte with a zero length field), so this // is guarded here rather than in every hint. if length == 0 && !matched { return Err(io::Error::new( io::ErrorKind::InvalidData, "PduHint reported a zero-length unmatched PDU; cannot make progress", )); }设计要点有三:
- 判定条件精确:只有
length == 0 && !matched才报错。匹配的零长度帧(matched == true)是合法协议形态,仍正常返回;未匹配但长度大于 0 的帧继续走原有的"读取并丢弃"路径; - 放在公共循环而非各
PduHint实现里:注释明确说明任何PduHint都可能产生该结果,因此在Framed层面统一防护,避免每个 hint 重复实现; - 错误类型恰当:使用
io::ErrorKind::InvalidData,语义是"输入数据非法、无法推进",调用方(single_sequence_step_read等)会通过custom_err!包装成连接器错误并终止连接,而不是继续空转。
回归测试:从上游 test 到 Warpgate 仓库的验证
该防护并非只停留在代码层面——Warpgate 在 warpgate-protocol-rdp/src/server/rdp.rs 中提供了一个专门的回归测试zero_length_unmatched_frame_fails_instead_of_spinning,其思路非常值得借鉴:
/// A fast-path header whose length field is zero is a frame `X224_HINT` neither /// matches nor can skip past. The reader has to fail it; looping on it would pin a /// core for as long as the process lives, long after the sender has gone away. #[tokio::test] async fn zero_length_unmatched_frame_fails_instead_of_spinning() { let (server_side, mut client_side) = tokio::io::duplex(64); client_side.write_all(&[0x00, 0x00]).await.unwrap(); let mut framed = TokioFramed::new(server_side); let result = tokio::time::timeout(Duration::from_secs(5), framed.read_by_hint(&X224_HINT)) .await .expect("read_by_hint spun instead of returning"); assert!(result.is_err()); }测试逻辑:向对端写入两个字节0x00 0x00(fast-path 头中零长度字段的畸形帧),随后用tokio::time::timeout包裹read_by_hint(&X224_HINT)。若补丁缺失,read_by_hint会无限空转,5 秒超时后expect触发 panic——测试恰好捕获"空转"这一症状;补丁生效后,函数在超时前以InvalidData错误返回,assert!(result.is_err())通过。测试注释点明这是典型的 DoS 场景:"looping on it would pin a core for as long as the process lives"。
该测试同时验证了补丁的普适性:X224_HINT是 RDP 协议最基础的帧 hint(warpgate-protocol-rdp/src/server/rdp.rs),它都无法匹配的输入,任何上层 hint 同样无法处理——防护必须落在Framed这一层。
补丁三(旁述):Cargo.toml的 lint 压制
PATCHES.md 最后一段记录了ironrdp-asyncfork 在Cargo.toml中的一处非源码改动:[lints.rust] 中设置warnings = { level = "allow", priority = 1 }(vendor/ironrdp-async/Cargo.toml)。其目的是让这份 vendored path dependency 的告警不在 Warpgate 自身构建中冒出来。
这一点有两个运维要点:
- 该改动不在
warpgate.patch里。warpgate.patch是纯源码(source-only)补丁,lint 压制是手工附加到Cargo.toml的; - 重新 vendor(re-vendor)时必须手工重放。由于
Cargo.toml是 Cargo 自动生成的规范化清单(文件头部有 "AUTOMATICALLY GENERATED BY CARGO" 标注),重新 vendor 上游时会丢失这份 lint 配置,需按 PATCHES.md 的提示手工重新应用。
这一设计也呼应了ironrdp-connectorfork 中的同类处理(vendor/ironrdp-connector/PATCHES.md)——两个 vendored 组件在 lint 策略上保持一致。
补丁生命周期与升级路径总结
综合两份 PATCHES.md,ironrdp-asyncfork 中所有改动的"脱落条件"如下表:
| 补丁 | 位置 | 上游回退条件 |
|---|---|---|
| Credential-less CredSSP | src/connector.rs 透传credssp_credentialless | 上游在Config上暴露等价开关(与ironrdp-connectorfork 一并移除) |
read_by_hint零长度防护 | src/framed.rs | vendored 基础版本 bump 到已包含上游修复的版本 |
Cargo.tomllint 压制 | Cargo.toml | 重新 vendor 后需手工重放(不在warpgate.patch中) |
对维护者而言,这意味着升级 IronRDP 依赖时不应机械地替换目录,而要对照 PATCHES.md 逐条核对:源码补丁是否已被上游吸收、Cargo.toml的 lint 配置是否需要手工重放、两个 fork(async 与 connector)是否必须同步更新。而对普通使用者来说,这些定制是透明的——你只需要知道interactive_logon目标选项可以安全地暴露服务器原生登录界面,而畸形 RDP 数据流不会再打满网关的 CPU。
延伸阅读
- 补丁说明文档:vendor/ironrdp-async/PATCHES.md(本文骨架)
- 源补丁(可重放):vendor/ironrdp-async/warpgate.patch
- 补丁落地的核心实现:vendor/ironrdp-async/src/connector.rs、vendor/ironrdp-async/src/framed.rs
- 配套的 connector fork:vendor/ironrdp-connector/PATCHES.md、vendor/ironrdp-connector/src/credssp.rs
- RDP 目标配置项定义:warpgate-common/src/config/target.rs
- 配置到 CredSSP 模式的翻译与回归测试:warpgate-protocol-rdp/src/client/mod.rs、warpgate-protocol-rdp/src/server/rdp.rs
- 网络安全
- 运维
- 认证鉴权
【免费下载链接】warpgate
Fully transparent SSH, HTTPS, Kubernetes, database and RDP/VNC bastion/PAM that doesn't need additional client-side software
相关推荐
IronRDP网络级别认证:深入理解CredSSP安全机制的完整解析
IronRDP网络级别认证:深入理解CredSSP安全机制的完整解析 IronRDP作为Rust实现的Microsoft远程桌面协议,其核心安全特性之一就是 网
网络通信音视频Bili.Copilot 登录机制全解析:扫码登录、网页登录与云视听小电视凭据体系
Bili.Copilot 登录机制全解析:扫码登录、网页登录与云视听小电视凭据体系 Bili.Copilot(哔哩助理)是一款基于 Windows App SD
桌面应用音视频AI 应用Semaphore 登录失败安全测试全解析:TC-002 无效凭证与账号枚举防护
Semaphore 登录失败安全测试全解析:TC 002 无效凭证与账号枚举防护 本文以仓库 test/test cases/TC 002 login inva
后端前端任务调度运维CI/CD认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考