IdMappedPortTCP 现在在 telnet 连接后需要 "prodding"

IdMappedPortTCP now requires "prodding" after telnet connection

多年来,我一直在特定程序中使用 IdMappedPortTCP 来允许通用端口转发。我正在测试升级后的 build/component 环境,但我 运行 遇到了问题。首先,这是新旧版本信息:

我正在使用标准 Windows 控制台 telnet 客户端和 Linux 服务器将它插入到 telnet 会话中来测试它,我发现行为发生了奇怪的变化。

这是事件链的比较:

旧:

6/08/2017  6:47:16 PM - DEBUG: MappedPort-Connect
6/08/2017  6:47:16 PM -   TCP Port Fwd: Connect: 127.0.0.1:4325 --> 127.0.0.1:23
6/08/2017  6:47:16 PM - DEBUG: MappedPort-OutboundConnect
6/08/2017  6:47:16 PM -   TCP Port Fwd: Outbound Connect: 192.168.214.11:4326 --> 192.168.210.101:23
6/08/2017  6:47:16 PM - DEBUG: MappedPort-OutboundData
6/08/2017  6:47:16 PM - DEBUG: MappedPort-Execute
6/08/2017  6:47:16 PM - DEBUG: MappedPort-OutboundData
6/08/2017  6:47:16 PM - DEBUG: MappedPort-Execute
6/08/2017  6:47:16 PM - DEBUG: MappedPort-OutboundData
...

新:

6/08/2017  6:41:34 PM - DEBUG: MappedPort-Connect
6/08/2017  6:41:34 PM -   TCP Port Fwd: Connect: 127.0.0.1:1085 --> 127.0.0.1:23
6/08/2017  6:41:34 PM - DEBUG: MappedPort-OutboundConnect
6/08/2017  6:41:34 PM -   TCP Port Fwd: Outbound Connect: 192.168.214.59:1086 --> 192.168.210.101:23
6/08/2017  6:47:36 PM - DEBUG: MappedPort-Execute
6/08/2017  6:47:36 PM - DEBUG: MappedPort-OutboundData
6/08/2017  6:47:36 PM - DEBUG: MappedPort-Execute
6/08/2017  6:47:36 PM - DEBUG: MappedPort-OutboundData
6/08/2017  6:47:36 PM - DEBUG: MappedPort-Execute

在第一个中,您在连接后立即看到 OutboundData。在第二个中,连接后没有任何反应,直到我发送击键(6 分钟后),此时您看到执行,然后是第一个 OutboundData 事件。

这引起了我的疑惑:是真的连接到服务器只是延迟了输出,还是连接本身被延迟了?

我的第一个结论是连接本身被延迟了,原因如下。服务器在登录提示时超时 1 分钟。如果您连接并收到问候语但只是坐在那里,服务器将在一分钟后断开连接。使用新的 Indy 版本,我在连接事件后坐在那里整整 6 分钟,然后毫无问题地收到了服务器问候语。

但是...NETSTAT 显示连接事件记录后不久就建立了与远程服务器的连接!因此,我只能得出连接确实已建立的结论,但也许某些初始字符是 "eaten" 或导致 getty 在获得击键之前不参与的内容?

有什么建议吗?您是否知道我可能会寻找的任何改变——我应该做但没有做的事情?任何见解表示赞赏。

(除非有任何好的线索,我想我侦查的下一步可能是用 WireShark 嗅探两台机器,看看连接后发生了什么。)

更新:Wireshark(单腿)

从机器外部捕获的数据包显示 MappedPort 与服务器之间的流量(但不是客户端与 MappedPort 之间的流量)显示 telnet 服务器向客户端发送 "Do Authenticate"(通过 MappedPort)回复 "Will Authenticate"。接下来是服务器发送身份验证子选项(并且客户端同意),然后是所有其他 telnet 选项。最后,在看到登录文本后,客户端发送 "do echo" 并且他们都坐在那里直到 1 分钟后,此时服务器发送 TCP FIN 以关闭连接。那是 "good old" 版本。

在新版本中,客户端不响应 "Will Authenticate",他们都无限期地坐在那里。 (嗯,我想知道这在服务器资源方面有什么关系——可能是好的 DOS 攻击。不过,它 一个旧的 telnet 守护程序,所以它可能是现在已修复...)当我最终发送第一个击键时,这就是它在该数据包中发送的全部内容。 THEN 客户端发送 "will authenticate"(没有来自服务器的额外提示)并且协商完全正常地继续;来自服务器的最后一个数据包(包含回显参数)也包含输入的回显字符。所以这就像客户端没有看到来自服务器的初始 "do authenticate" 数据包,但是一旦你开始输入,就会继续并像刚刚听到它一样响应(一旦它发送击键)。

6/13更新:Wireshark(两条腿)

我捕获了 "broken" 对话的两条腿并对其进行了分析。有趣的行为。底线: 一旦服务器获得 TCP 连接,它就会发回 Telnet-DoAuth 邀请。 IdMappedPortTCP 保留该数据包并且不会将其传递给客户端——还没有。一旦客户端最终发送了第一次击键(几秒或几分钟后),Id 将其传递给服务器。 然后 Id 将从服务器获得的 DoAuth 数据包传递给客户端。

下面是数据包的更详细统计:

65 11-59 TCP Syn
67 59-11 TCP SynAck
69 11-59 TCP Ack
71 59-101 TCP Syn
73 101-59 TCP SynAck
74 59-101 TCP Ack
76 101-59 DoAuth
77 59-101 TCP Ack
nothing for 23 seconds
79 11-59 Data:\r\n (I pressed Enter)
81 59-101 Data:\r\n
83 59-11 DoAuth
85 11-59 WillAuth
87 101-59 TCP Ack
88 59-101 WillAuth
90 101-59 TCP Ack
91 101-59 Authentication option
92 59-11 Authentication option
94 11-59 Authentication option reply
96 59-101 Authentication option reply
98 101-59 Will/do Encryption/terminal/env options
99 59-101 Will/do Encryption/terminal/env options
101 11-59 Don't encrypt
103 59-101 Don't encrypt
105 101-59 TCP Ack
106 59-11 TCP Ack
108 11-59 Won't/will list
110 59-101 Won't/will list
112 101-59 TCP Ack
113 101-59 Do window size
114 59-11 Do window size

数据包转储行格式: Pkt# From-To Payload

(不要介意 packet# skips;客户端和代理都 运行ning 在由我 运行ning 从中捕获的机器托管的 VM 上,所以 Wireshark 看到了两个数据包的副本。我只包含了 pkt#,这样我以后可以根据需要参考原始转储。)

From/To 机器:

10 = Linux client (see below)
11 = Windows client
59 = proxy
101 = server

一个有趣的转移:Linux客户

尽管我所有的测试都使用了各种 Windows 客户端(因为那是生产中使用的),我 "accidentally" 使用了 Linux(因为那是我 运行在我的工作站上,我 运行 Wireshark)因为它很方便。那个客户的行为不同——更积极——从而避免了这个问题。下面是它的转储:

1 10-59 TCP Syn
2 59-10 TCP SynAck
3 10 59 TCP Ack
4 10-59 Do/Will list
5 59-101 TCP Syn
7 101-59 TCP SynAck
8 59-101 TCP Ack
10 59-101 Do/Will list
12 101-59 TCP Ack
13 101-59 DoAuth
14 59-10 DoAuth
15 10-59 TCP Auth
16 10-59 WontAuth
17 59-101 WontAuth
19 101-59 Will/Do list
20 59-10 Will/Do list
21 10-50 Do window size
22 59-101 Do window size

如您所见,客户端不会等待 telnet 服务器先说话——一旦建立 TCP 连接,它就会发送一个完整的 Do/Will 列表。一旦 Id 打开该连接,这又会传递到服务器。服务器发回与之前一样的"DoAuth";不同之处在于,这一次,已经传递了来自客户端的流量,Id 立即传递它。客户端然后发送身份验证标志,然后事情继续进行。

所以,如果客户端先说话,IdMappedPortTCP 没问题;只有当服务器首先发言时,它才会保留其消息,并且不会将其传递给客户端,直到客户端说些什么。

9/27 更新:发现代码更改

降级到 9.0.0.14 解决了这个问题。比较两个版本的 IdMappedPortTCP.pas 的源代码,我发现唯一的区别是较新的版本在 过程中添加了一段代码 TIdMappedPortThread.OutboundConnect:

  DoOutboundClientConnect(Self);

  FNetData := Connection.CurrentReadBuffer;
  if Length(FNetData) > 0 then begin
    DoLocalClientData(Self);
    FOutboundClient.Write(FNetData);
  end;//if

except

(第一行和最后一行已经存在,仅显示上下文。)

我确认将该代码添加到 9.0.0.14 会产生问题。

我检查了 SVN 存储库,您在 2008 年 9 月 7 日添加了违规代码。提交评论是:

Updated TIdMappedPortThread.OutboundConnect() to check for pending data in the inbound client's InputBuffer after the OnOutboundConnect event handler exits.

我不完全理解更改的原因或含义 - 显然你这样做有充分的理由 - 但它确实产生了我描述的效果("holding onto" 服务器的初始输出,直到客户端发送一些东西)。

在 Indy 9 中,TIdTCPConnection.CurrentReadBuffer() 调用 TIdTCPConnection.ReadFromStack() 然后返回存储在 TIdTCPConnection.InputBuffer 中的任何数据 属性:

function TIdTCPConnection.CurrentReadBuffer: string;
begin
  Result := '';
  if Connected then begin
    ReadFromStack(False); // <-- here
  end;
  Result := InputBuffer.Extract(InputBuffer.Size);
end;

不管 InputBuffer 中可能已经存在什么,ReadFromStack() 等待套接字接收 new 数据以附加到 InputBuffer.它不会退出,直到新数据实际到达,或指定的 ReadTimeout 时间间隔过去。 TIdTCPConnection.ReadTimeout 属性 默认设置为 0,因此当 CurrentReadBuffer() 调用 ReadFromStack() 时,它最终使用无限超时:

function TIdTCPConnection.ReadFromStack(const ARaiseExceptionIfDisconnected: Boolean = True;
  ATimeout: Integer = IdTimeoutDefault; const ARaiseExceptionOnTimeout: Boolean = True): Integer;
// Reads any data in tcp/ip buffer and puts it into Indy buffer
// This must be the ONLY raw read from Winsock routine
// This must be the ONLY call to RECV - all data goes thru this method
var
  i: Integer;
  LByteCount: Integer;
begin
  if ATimeout = IdTimeoutDefault then begin
    if ReadTimeOut = 0 then begin
      ATimeout := IdTimeoutInfinite; // <-- here
    end else begin
      ATimeout := FReadTimeout;
    end;
  end;
  ...
end;

因此,当 TIdMappedPortTCP.OutboundConnect() 在将其 OutboundClient 连接到服务器后调用 CurrentReadBuffer() 时,它确实会等待数据从客户端到达,然后再从服务器读取数据。为避免这种情况,您可以在 TIdMappedPortTCP.OnConnectTIdMappedPortTCP.OnOutboundConnect 事件中设置一个非无限的 ReadTimeout 值,例如:

AThread.Connection.ReadTimeout := 1;

在 Indy 10 中,此问题已在 TIdMappedPortTCP 中修复,方法是避免在连接到服务器后对客户端数据进行初始等待。我现在已经在 Indy 9 中更新了 TIdMappedPortTCP 来做同样的事情。