Есть как минимум две проблемы с OnionProtocol
:
- самый внутренний
TLSMemoryBIOProtocol
становится wrappedProtocol
, когда он должен быть самый внешний ;
ProtocolWithoutConnectionLost
не извлекает TLSMemoryBIOProtocol
s из стека OnionProtocol
, потому что connectionLost
вызывается только после того, как методы FileDescriptor
s doRead
или doWrite
возвращают причину отключения.
Мы не можем решить первую проблему, не изменив способ управления OnionProtocol
его стеком, и мы не можем решить вторую, пока не выясним реализацию нового стека. Неудивительно, что правильный дизайн является прямым следствием потоков данных в Twisted, поэтому мы начнем с некоторого анализа потока данных.
Twisted представляет собой установленное соединение с экземпляром twisted.internet.tcp.Server
или twisted.internet.tcp.Client
. Поскольку единственная интерактивность в нашей программе происходит в stoptls_client
, мы будем рассматривать только поток данных в Client
экземпляр и из него.
Давайте прогреемся с минимальным клиентом LineReceiver
, который отображает обратные линии, полученные от локального сервера через порт 9999:
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)
Как только установленное соединение установлено, Client
становится транспортом нашего протокола LineReceiver
и опосредует ввод и вывод:
![Client and LineReceiver](https://i.stack.imgur.com/gTPRa.png)
Новые данные с сервера приводят к тому, что реактор вызывает метод Client
doRead
, который, в свою очередь, передает полученное значение методу LineReceiver
dataReceived
. Наконец, LineReceiver.dataReceived
вызывает LineReceiver.lineReceived
, когда доступна хотя бы одна линия.
Наше приложение отправляет строку данных обратно на сервер, вызывая LineReceiver.sendLine
. Это вызывает write
для транспорта, привязанного к экземпляру протокола, который является тем же экземпляром Client
, который обрабатывал входящие данные. Client.write
обеспечивает отправку данных реактором, тогда как Client.doWrite
фактически отправляет данные через сокет.
Мы готовы взглянуть на поведение OnionClient
, которое никогда не вызывает startTLS
:
![OnionClient without startTLS](https://i.stack.imgur.com/yP5FC.png)
OnionClient
s заключены в OnionProtocol
s , что является сутью нашей попытки вложенного TLS. В качестве подкласса twisted.internet.policies.ProtocolWrapper
, экземпляр OnionProtocol
является своего рода сэндвичем для передачи протокола; он представляет собой протокол для транспорта более низкого уровня и транспорт для протокола, который он оборачивает через маскарад, установленный во время соединения с помощью WrappingFactory
.
Теперь Client.doRead
вызывает OnionProtocol.dataReceived
, который передает данные до OnionClient
. В качестве транспорта OnionClient
, OnionProtocol.write
принимает строки для отправки из OnionClient.sendLine
и передает их до Client
, это собственный транспорт. Это нормальное взаимодействие между ProtocolWrapper
, его обернутым протоколом и его собственным транспортом, поэтому, естественно, данные передаются друг другу без каких-либо проблем.
OnionProtocol.startTLS
делает что-то другое. Он пытается вставить новую ProtocolWrapper
- которая оказывается TLSMemoryBIOProtocol
- между установленной протокольно-транспортной парой. Это кажется достаточно простым: ProtocolWrapper
хранит протокол верхнего уровня как его wrappedProtocol
атрибут , а прокси write
и другие атрибуты вплоть до своего собственного транспорта . startTLS
должен иметь возможность вставлять новый TLSMemoryBIOProtocol
, который включает OnionClient
в соединение, путем установки этого экземпляра поверх его собственных wrappedProtocol
и 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
:
![startTLS one TLSMemoryBIOProtocol, working](https://i.stack.imgur.com/LxhOA.png)
Как и ожидалось, новые данные, доставленные в OnionProtocol.dataReceived
, направляются в TLSMemoryBIOProtocol
, хранящийся в _tlsStack
, который передает расшифрованный открытый текст в OnionClient.dataReceived
. OnionClient.sendLine
также передает свои данные в TLSMemoryBIOProtocol.write
, который шифрует их и отправляет полученный зашифрованный текст в OnionProtocol.write
, а затем Client.write
.
К сожалению, эта схема не работает после второго вызова startTLS
. Основной причиной является эта строка:
self.wrappedProtocol = self.transport = tlsProtocol
Каждый вызов startTLS
заменяет wrappedProtocol
на самый внутренний TLSMemoryBIOProtocol
, хотя данные, полученные Client.doRead
, были зашифрованы самым внешним :
![startTLS two TLSMemoryBIOProtocols, broken](https://i.stack.imgur.com/2MbhO.png)
transport
, однако, вложены правильно.OnionClient.sendLine
может вызывать только свой транспортный write
- то есть OnionProtocol.write
- поэтому OnionProtocol
должен заменить его transport
на самый внутренний TLSMemoryBIOProtocol
, чтобы гарантировать, что записи последовательно вложены в дополнительные уровни шифрования.
Решение, таким образом, состоит в том, чтобы обеспечить передачу данных через первый TLSMemoryBIOProtocol
на _tlsStack
к следующему по очереди, так, чтобы каждый уровень шифрованияснимается в обратном порядке:
![startTLS with two TLSMemoryBIOProtocols, working](https://i.stack.imgur.com/uy2gO.png)
Представление _tlsStack
в виде списка кажется менее естественным, учитывая это новое требование.К счастью, представление входящего потока данных линейно предполагает новую структуру данных:
![Incoming data as a linked list traversal](https://i.stack.imgur.com/JuSus.png)
И глючный, и правильный поток входящих данных напоминают односвязный списокс wrappedProtocol
, служащим в качестве ProtocolWrapper
следующих ссылок, и protocol
, служащим в качестве Client
.Список должен расти вниз от OnionProtocol
и всегда заканчиваться OnionClient
.Ошибка возникает из-за того, что инвариант упорядочения нарушен.
Односвязный список хорош для проталкивания протоколов в стек, но неудобен для их удаления, поскольку для его удаления требуется обход вниз от его головы к узлу.Конечно, этот обход происходит каждый раз, когда данные получены, поэтому проблема заключается в сложности, связанной с дополнительным обходом, а не в наихудшей временной сложности.К счастью, список фактически связан дважды:
![Doubly linked list with protocols and transports](https://i.stack.imgur.com/i2p7t.png)
Атрибут transport
связывает каждый вложенный протокол со своим предшественником, так что transport.write
может слойна последовательно более низких уровнях шифрования перед окончательной отправкой данных по сети.У нас есть два стража, чтобы помочь в управлении списком: Client
всегда должен быть вверху и OnionClient
всегда должен быть внизу.
Соединяя их вместе, мы получим следующее:
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, которые не закрывали базовый сокет.
На момент написания этой статьи TLSMemoryBIOProtocol
называет свой транспортный номер loseConnection
точно в двух местах: _shutdownTLS
и _tlsShutdownFinished
._shutdownTLS
вызывается при активном закрытии (loseConnection
, abortConnection
, unregisterProducer
и после loseConnection
и всех ожидающих записейбыли сброшены ), в то время как _tlsShutdownFinished
вызывается при пассивном закрытии ( отказы рукопожатия , пустые чтения , ошибки чтения и записьошибки ).Все это означает, что обе стороны закрытого соединения могут вытолкнуть остановленные сеансы TLS из стека во время loseConnection
.PopOnDisconnectTransport
делает это идемпотентно, потому что loseConnection
, как правило, идемпотентно, и TLSMemoryBIOProtocol
определенно ожидает, что оно будет.
Недостатком использования логики стекового управления в loseConnection
является то, что она зависит от особенностей *Реализация 1267 *.Для обобщенного решения потребуются новые API на многих уровнях Twisted.
До этого мы застряли с другим примером закона Хайрама .