Skip to content

fix: harden Telnet login stream boundaries and trace redaction - #10

Merged
LeeEirc merged 3 commits into
masterfrom
fix/telnet-login-stream-boundaries
Nov 3, 2026
Merged

LeeEirc merged 3 commits into
masterfrom
fix/telnet-login-stream-boundaries

Conversation

@LeeEirc

@LeeEirc LeeEirc commented Nov 3, 2026

Copy link
Copy Markdown
Owner

背景

基于最新 master 的 5f31db2d5c7351b1f8a5424128b8d638d02415c7(已合并 #9)。修复审查中发现的流式前缀误判、登录 Trace 子协商参数泄露,以及超长子协商破坏 framing 的问题。

不新增公开 API、配置字段或通用 Expect 接口;不修改 Koko。

1. 批量扫描,并区分完整消息与未换行提示

  • 保留现有 4 KiB bufio.Reader。用 Peek(1) 在缓冲区为空时触发填充,再扫描 Peek(Buffered()),只 Discard 已消费的字节。
  • 不再在登录循环中 ReadByte 后对每个字符前缀执行全部正则;不等待凑满 4096 字节,也不引入固定 sleep。
  • 文本归一化仍采用轻量逐字节状态转换,保留跨 chunk 的 ANSI、UTF-8、CR/LF、退格状态。
  • 叙述性失败/密码过期消息:只在 CRLF/LF 完成逻辑行后判断;EOF 或总登录超时再判断最终未换行的错误消息。结束连接时不会再发送凭据或判定成功。
  • 交互提示:在当前可用 chunk 扫描完成后,对最长 512 字节的当前提示做匹配。未换行的改密输入要求以 : / : 结束。
  • 修复 Authentication failed count: 0、Password expired policy: disabled、New password policy: 90 days 等文本在前缀处提前失败。
  • 补充中文 Banner 过滤,阻止 登录成功 >、密码错误>、系统警告 > 和 [Warning]# 被默认规则当成 shell。
  • 超长逻辑行直接忽略到下一次行重置/结束,不使用截断后的尾部制造伪提示符,也不再逐字节复制整行。
  • 登录超时与成功发生竞态时,不返回已经被定时器关闭的 Client。

不丢失 prompt 后的数据

只丢弃已消费的 chunk 前缀,后续数据仍留在同一个 reader。为了确认完整行而消费的 CR/LF(最多两字节)会在会话读取前补回。例如 router#\r\nALARM 登录后仍可读取 \r\nALARM,已匹配 prompt 不重复回放。

说明:无换行文本提示仍然有固有歧义,缓冲区大小或固定延时不能提供协议级认证保证。异常设备应使用精确的平台提示配置;本 PR 不声称识别任意文本协议。

2. 在 tracer 层统一执行登录脱敏

  • wire/application 双向数据以及收到、生成的 SB 参数统一脱敏。
  • NEW_ENVIRON USER 不再绕过字节日志脱敏,通过 packet/event 日志输出用户名。
  • 保留命令名、选项名、长度及登录状态等诊断信息。
  • 在调用 WrapStream 前启用脱敏;失败后的 writer Close/flush 仍保持脱敏。
  • 登录成功后恢复原有 Trace 行为。会话命令、输出及独立 monitor 仍可能包含敏感内容,不能作为默认生产日志。

3. 超长 SB 丢弃到真实结束符

  • 达到上限后返回 ErrSubnegotiationTooLong,进入 discard 状态,而不是直接恢复普通数据状态。
  • 跨读取一直丢弃到真实 IAC SE;IAC IAC 是转义负载,不会错误结束子协商。
  • 继续扫描当前 chunk,保留其后的正常数据及合法协商。
  • 调用方在错误后继续读,也不会把超长 SB 尾部当作终端输出。

提交拆分

  1. fix: match login events at stream boundaries
  2. fix: redact all login trace payloads centrally
  3. fix: discard oversized subnegotiations without losing framing

验证

新增端到端/表驱动测试覆盖:1/2/7/1024/4096 字节分块、每个拆分点、失败/过期统计 Banner、中文 Banner、无换行交互、Continue、EOF/超时、UTF-8/ANSI、长行、prompt 后数据、环境变量子协商日志及失败清理写入。

新增两个 fuzz target:

FuzzLoginDiagnosticChunking
FuzzOversizedSubnegotiationRecovery

新增 BenchmarkLoginLongBanner,覆盖 1 KiB、4 KiB、64 KiB Banner。

新增回归用例已在本地 Go 1.23.2 上通过 race 重复执行、vet 和两组 5 秒 fuzz。全量原有与新增用例由本 PR 的 Go 1.20.x / stable CI 验证,包含示例客户端及 monitor 编译。

设计边界与行为说明见 docs/login-stream.md。

Scan available buffered chunks instead of checking every byte prefix. Defer narrative failures and expiry messages until line completion, preserve post-prompt data, bound partial matching, and add stream regressions and fuzz coverage.
Protect incoming and generated subnegotiation parameters as well as wire/application data. Keep redaction active during stream-wrapper setup and failed-login cleanup.
Continue through the real IAC SE across reads, honor escaped IAC, preserve following application data and valid events, and cover split points with regression and fuzz tests.
Copilot AI balanced review requested due to automatic review settings November 3, 2026 10:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Authentication failures can be misclassified as successful logins when line endings arrive in a later read, while valid long custom prompts and structured bracket prompts can be rejected.

3 open issues
What changed in this PR

This PR hardens Telnet authentication and stream handling without changing the public API.

Changes:

  • Matches login messages at logical-line boundaries, bounds partial prompts, and preserves buffered session data.
  • Centralizes login trace redaction, including subnegotiation payloads and failed-login cleanup.
  • Preserves framing after oversized subnegotiations, with regression tests, fuzz targets, and a benchmark.
File Description
transport.go Delegates payload redaction to the tracer.
trace.go Centralizes byte and subnegotiation payload redaction.
trace_secrecy_test.go Tests login secrecy and cleanup-write redaction.
login.go Adds boundary-aware scanning, banner filtering, and timeout-race handling.
login_text.go Extracts streaming normalization and ignores overlong lines.
login_stream_test.go Tests chunk boundaries and session-data preservation; adds fuzzing and benchmarking.
docs/​login-stream.md Documents stream behavior, limitations, and diagnostics.
decoder.go Discards oversized subnegotiations through their actual terminator.
decoder_recovery_test.go Tests and fuzzes framing recovery.
client.go Enables redaction before wrapping and replays consumed prompt terminators.

Review effort: Balanced

Comment thread login.go
}
}

result, err := m.handlePrompt(writer, text.current(), trace)
Comment thread login.go
Comment on lines +190 to +191
if len(line) > maxPartialPromptBytes || !utf8.Valid(line) {
return loginPending, nil
Comment thread login.go
Comment on lines +264 to +265
if isBannerLikeShellPrompt(string(line)) {
return false
@LeeEirc
LeeEirc merged commit e035360 into master Nov 3, 2026
5 checks passed

LeeEirc commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

已逐条复现本 PR 的三条 Copilot 评论,并基于合并后的 master e035360 提交后续修复:#11。

  • 未完成的失败/改密叙述保持 pending,保留文本和阶段,不能继续落入 Shell 或发送凭据/Continue 的分支;CRLF、EOF、超时后再判断最终结果。
  • 512 字节上限只用于未完成提示;完整自定义提示使用逻辑行上限,默认 Shell 仍保留自己的长度限制。
  • 结构化 [user@host path]$ 提示先于 Banner 词过滤,避免用户名、主机名、目录中的 login/error 被误伤;[Warning]# 等标签继续拒绝。

新增用例先在未修复实现上确认失败,修复后通过,并覆盖分开的 CR/LF、所有拆分点、EOF/超时、自定义规则重叠及长提示。#11 尚未合并,本 PR 的线程暂不标记为已解决。

@LeeEirc
LeeEirc deleted the fix/telnet-login-stream-boundaries branch November 3, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants