如果您对两个层使用相同的TLS参数并且要连接到同一主机,那么您可能对两个加密层使用相同的密钥对。尝试为嵌套层使用不同的密钥对,例如隧道连接到第三个主机/端口。即: localhost:30000 (客户) - > localhost:8080 (使用密钥对A的TLS层1) - > localhost:8081 (TLS第2层使用密钥对B)。
localhost:30000
localhost:8080
localhost:8081
如果该设备具有该功能,您可能需要通知远程设备您希望启动环境并在启动之前为第二层分配资源。
至少有两个问题 OnionProtocol :
OnionProtocol
TLSMemoryBIOProtocol
wrappedProtocol
ProtocolWithoutConnectionLost
connectionLost
FileDescriptor
doRead
doWrite
如果不改变方式,我们就无法解决第一个问题 OnionProtocol 管理它的堆栈,我们无法解决第二个问题,直到我们找到新的堆栈实现。不出所料,正确的设计是Twisted中数据流动方式的直接结果,因此我们将从一些数据流分析开始。
Twisted表示与其中一个实例建立的连接 twisted.internet.tcp.Server 要么 twisted.internet.tcp.Client 。由于我们程序中唯一的交互发生在 stoptls_client ,我们只会考虑进出数据流 Client 实例。
twisted.internet.tcp.Server
twisted.internet.tcp.Client
stoptls_client
Client
让我们用最小的热身 LineReceiver 在端口9999上回送从本地服务器收到的线路的客户端:
LineReceiver
from twisted.protocols import basic from twisted.internet import defer, endpoints, protocol, task class LineReceiver(basic.LineReceiver): def lineReceived(self, line): self.sendLine(line) def main(reactor): clientEndpoint = endpoints.clientFromString( reactor, "tcp:localhost:9999") connected = clientEndpoint.connect( protocol.ClientFactory.forProtocol(LineReceiver)) def waitForever(_): return defer.Deferred() return connected.addCallback(waitForever) task.react(main)
一旦建立连接建立,a Client 成为我们的 LineReceiver 协议的传输和中介输入和输出:
来自服务器的新数据导致reactor调用 Client 的 doRead 方法,它反过来传递它收到的东西 LineReceiver 的 dataReceived 方法。最后, LineReceiver.dataReceived 电话 LineReceiver.lineReceived 当至少有一行可用时。
dataReceived
LineReceiver.dataReceived
LineReceiver.lineReceived
我们的应用程序通过调用将一行数据发送回服务器 LineReceiver.sendLine 。这叫 write 在绑定到协议实例的传输上,这是相同的 Client 处理传入数据的实例。 Client.write 安排反应堆发送数据,同时 Client.doWrite 实际上通过套接字发送数据。
LineReceiver.sendLine
write
Client.write
Client.doWrite
我们准备好看看一个人的行为 OnionClient 从不打电话 startTLS :
OnionClient
startTLS
OnionClient 被包裹着 OnionProtocol 小号 ,这是我们尝试嵌套TLS的关键。作为的子类 twisted.internet.policies.ProtocolWrapper ,一个例子 OnionProtocol 是一种协议传输三明治;它表现为一个 协议 到较低级别的运输和作为 运输 通过一个协议,它通过一个在连接时建立的伪装来包装 WrappingFactory 。
twisted.internet.policies.ProtocolWrapper
WrappingFactory
现在, Client.doRead 电话 OnionProtocol.dataReceived ,代理数据到 OnionClient 。如 OnionClient 的运输, OnionProtocol.write 接受要发送的行 OnionClient.sendLine 并代理他们 Client , 它的 拥有 运输。这是a之间的正常交互 ProtocolWrapper ,它的包装协议,以及它自己的传输,所以数据自然流动每个人都没有任何麻烦。
Client.doRead
OnionProtocol.dataReceived
OnionProtocol.write
OnionClient.sendLine
ProtocolWrapper
OnionProtocol.startTLS 有所不同。它试图设置一个新的 ProtocolWrapper - 恰好是一个 TLSMemoryBIOProtocol - 之间 既定 协议传输对。这似乎很容易:a ProtocolWrapper 将上层协议存储为其 wrappedProtocol 属性 ,和 代理 write 和其他属性归结为自己的传输 。 startTLS 应该能够注入新的 TLSMemoryBIOProtocol 包裹 OnionClient 通过修补自己的实例来进入连接 wrappedProtocol 和 transport :
OnionProtocol.startTLS
transport
def startTLS(self): ... connLost = ProtocolWithoutConnectionLost(self.wrappedProtocol) connLost.onion = self # Construct a new TLS layer, delivering events and application data to the # wrapper just created. tlsProtocol = TLSMemoryBIOProtocol(None, connLost, False) # Push the previous transport and protocol onto the stack so they can be # retrieved when this new TLS layer stops. self._tlsStack.append((self.transport, self.wrappedProtocol)) ... # Make the new TLS layer the current protocol and transport. self.wrappedProtocol = self.transport = tlsProtocol
这是第一次调用后的数据流 startTLS :
正如所料,新数据传递给了 OnionProtocol.dataReceived 被送到了 TLSMemoryBIOProtocol 存储在 _tlsStack ,将解密的明文传递给 OnionClient.dataReceived 。 OnionClient.sendLine 也将其数据传递给 TLSMemoryBIOProtocol.write ,对其进行加密并将生成的密文发送给 OnionProtocol.write 然后 Client.write 。
_tlsStack
OnionClient.dataReceived
TLSMemoryBIOProtocol.write
不幸的是,这个方案在第二次调用后失败了 startTLS 。根本原因是这一行:
self.wrappedProtocol = self.transport = tlsProtocol
每次打电话 startTLS 取代了 wrappedProtocol 随着 内 TLSMemoryBIOProtocol ,即使收到的数据 Client.doRead 被加密了 最 :
该 transport 但是,s是正确嵌套的。 OnionClient.sendLine 只能叫它的运输工具 write - 那是, OnionProtocol.write - 所以 OnionProtocol 应该取代它 transport 与最里面的 TLSMemoryBIOProtocol 确保写入连续嵌套在其他加密层中。
因此,解决方案是确保数据流过 第一 TLSMemoryBIOProtocol 在...上 _tlsStack 到了 下一个 反过来,以便按照应用的相反顺序剥离每层加密:
代表 _tlsStack 鉴于这一新要求,列表似乎不太自然。幸运的是,线性表示传入的数据流表明了一种新的数据结构:
传输数据的错误和正确流程都类似于单链表 wrappedProtocol 担任 ProtocolWrapper 下一个链接和 protocol 担任 Client 的。该列表应该从下降 OnionProtocol 并始终以 OnionClient 。发生该错误是因为违反了排序不变量。
protocol
一个单链表可以很好地将协议推送到堆栈上但是很难将它们弹出,因为它需要从头部向下移动到节点才能删除。当然,每次收到数据时都会发生这种遍历,因此关注的是额外遍历所隐含的复杂性,而不是最坏情况下的时间复杂度。幸运的是,该列表实际上是双重关联的:
该 transport 属性链接每个嵌套协议与其前身,所以 transport.write 在最终通过网络发送数据之前,可以连续降低加密级别。我们有两个哨兵来帮助管理清单: Client 必须始终在顶部 OnionClient 必须始终在底部。
transport.write
把两者放在一起,我们最终得到这个:
from twisted.python.components import proxyForInterface from twisted.internet.interfaces import ITCPTransport from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol from twisted.protocols.policies import ProtocolWrapper, WrappingFactory class PopOnDisconnectTransport(proxyForInterface(ITCPTransport)): """ L{TLSMemoryBIOProtocol.loseConnection} shuts down the TLS session and calls its own transport's C{loseConnection}. A zero-length read also calls the transport's C{loseConnection}. This proxy uses that behavior to invoke a C{pop} callback when a session has ended. The callback is invoked exactly once because C{loseConnection} must be idempotent. """ def __init__(self, pop, **kwargs): super(PopOnDisconnectTransport, self).__init__(**kwargs) self._pop = pop def loseConnection(self): self._pop() self._pop = lambda: None class OnionProtocol(ProtocolWrapper): """ OnionProtocol is both a transport and a protocol. As a protocol, it can run over any other ITransport. As a transport, it implements stackable TLS. That is, whatever application traffic is generated by the protocol running on top of OnionProtocol can be encapsulated in a TLS conversation. Or, that TLS conversation can be encapsulated in another TLS conversation. Or **that** TLS conversation can be encapsulated in yet *another* TLS conversation. Each layer of TLS can use different connection parameters, such as keys, ciphers, certificate requirements, etc. At the remote end of this connection, each has to be decrypted separately, starting at the outermost and working in. OnionProtocol can do this itself, of course, just as it can encrypt each layer starting with the innermost. """ def __init__(self, *args, **kwargs): ProtocolWrapper.__init__(self, *args, **kwargs) # The application level protocol is the sentinel at the tail # of the linked list stack of protocol wrappers. The stack # begins at this sentinel. self._tailProtocol = self._currentProtocol = self.wrappedProtocol def startTLS(self, contextFactory, client, bytes=None): """ Add a layer of TLS, with SSL parameters defined by the given contextFactory. If *client* is True, this side of the connection will be an SSL client. Otherwise it will be an SSL server. If extra bytes which may be (or almost certainly are) part of the SSL handshake were received by the protocol running on top of OnionProtocol, they must be passed here as the **bytes** parameter. """ # The newest TLS session is spliced in between the previous # and the application protocol at the tail end of the list. tlsProtocol = TLSMemoryBIOProtocol(None, self._tailProtocol, False) tlsProtocol.factory = TLSMemoryBIOFactory(contextFactory, client, None) if self._currentProtocol is self._tailProtocol: # This is the first and thus outermost TLS session. The # transport is the immutable sentinel that no startTLS or # stopTLS call will move within the linked list stack. # The wrappedProtocol will remain this outermost session # until it's terminated. self.wrappedProtocol = tlsProtocol nextTransport = PopOnDisconnectTransport( original=self.transport, pop=self._pop ) # Store the proxied transport as the list's head sentinel # to enable an easy identity check in _pop. self._headTransport = nextTransport else: # This a later TLS session within the stack. The previous # TLS session becomes its transport. nextTransport = PopOnDisconnectTransport( original=self._currentProtocol, pop=self._pop ) # Splice the new TLS session into the linked list stack. # wrappedProtocol serves as the link, so the protocol at the # current position takes our new TLS session as its # wrappedProtocol. self._currentProtocol.wrappedProtocol = tlsProtocol # Move down one position in the linked list. self._currentProtocol = tlsProtocol # Expose the new, innermost TLS session as the transport to # the application protocol. self.transport = self._currentProtocol # Connect the new TLS session to the previous transport. The # transport attribute also serves as the previous link. tlsProtocol.makeConnection(nextTransport) # Left over bytes are part of the latest handshake. Pass them # on to the innermost TLS session. if bytes is not None: tlsProtocol.dataReceived(bytes) def stopTLS(self): self.transport.loseConnection() def _pop(self): pop = self._currentProtocol previous = pop.transport # If the previous link is the head sentinel, we've run out of # linked list. Ensure that the application protocol, stored # as the tail sentinel, becomes the wrappedProtocol, and the # head sentinel, which is the underlying transport, becomes # the transport. if previous is self._headTransport: self._currentProtocol = self.wrappedProtocol = self._tailProtocol self.transport = previous else: # Splice out a protocol from the linked list stack. The # previous transport is a PopOnDisconnectTransport proxy, # so first retrieve proxied object off its original # attribute. previousProtocol = previous.original # The previous protocol's next link becomes the popped # protocol's next link previousProtocol.wrappedProtocol = pop.wrappedProtocol # Move up one position in the linked list. self._currentProtocol = previousProtocol # Expose the new, innermost TLS session as the transport # to the application protocol. self.transport = self._currentProtocol class OnionFactory(WrappingFactory): """ A L{WrappingFactory} that overrides L{WrappingFactory.registerProtocol} and L{WrappingFactory.unregisterProtocol}. These methods store in and remove from a dictionary L{ProtocolWrapper} instances. The C{transport} patching done as part of the linked-list management above causes the instances' hash to change, because the C{__hash__} is proxied through to the wrapped transport. They're not essential to this program, so the easiest solution is to make them do nothing. """ protocol = OnionProtocol def registerProtocol(self, protocol): pass def unregisterProtocol(self, protocol): pass
(这也是可用的 GitHub上 。)
第二个问题的解决方案在于 PopOnDisconnectTransport 。原始代码尝试从堆栈中弹出TLS会话 connectionLost ,但因为只有一个封闭的文件描述符导致 connectionLost 要调用,它无法删除未关闭底层套接字的已停止的TLS会话。
PopOnDisconnectTransport
在撰写本文时, TLSMemoryBIOProtocol 叫它的运输 loseConnection 在两个地方: _shutdownTLS 和 _tlsShutdownFinished 。 _shutdownTLS 在主动关闭时调用( loseConnection , abortConnection , unregisterProducer 和 后 loseConnection 并且所有挂起的写入都已刷新 ),同时 _tlsShutdownFinished 在被动关闭时调用( 握手失败 , 空读 , 读错误 ,和 写错误 )。这一切都意味着 都 关闭连接的两侧可以在执行期间从堆栈中弹出TLS会话 loseConnection 。 PopOnDisconnectTransport 这是无意义的,因为 loseConnection 通常是幂等的,并且 TLSMemoryBIOProtocol 当然希望它是。
loseConnection
_shutdownTLS
_tlsShutdownFinished
abortConnection
unregisterProducer
将堆栈管理逻辑放入的缺点 loseConnection 这取决于具体情况 TLSMemoryBIOProtocol 的实施。通用解决方案需要跨越多个Twisted级别的新API。
在那之前,我们仍然坚持另一个例子 Hyrum定律 。