Repository: twisted/twisted

Issue:
builtins.OSError when using HTTP server over unix sockets since Twisted 24.10.0
Since 24.10.0 it is not possible to use HTTP server over `unix:` protocol. 

This bug has been initially reported in https://github.com/buildbot/buildbot/issues/8207.

Relevant log:
```
2024-11-12 05:54:43+0000 [buildbot.www.service.BuildbotSite] Unhandled Error
        Traceback (most recent call last):
          File "/home/buildbot/venv/lib64/python3.13/site-packages/twisted/internet/tcp.py", line 1423, in doRead
            protocol.makeConnection(transport)
          File "/home/buildbot/venv/lib64/python3.13/site-packages/twisted/internet/protocol.py", line 509, in makeConnection
            self.connectionMade()
          File "/home/buildbot/venv/lib64/python3.13/site-packages/twisted/web/http.py", line 2336, in connectionMade
            self.transport.setTcpNoDelay(True)
          File "/home/buildbot/venv/lib64/python3.13/site-packages/twisted/internet/tcp.py", line 340, in setTcpNoDelay
            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, enabled)
        builtins.OSError: [Errno 95] Operation not supported
```

Relevant Twisted configuration:
```
    port='unix:/run/buildbot/buildbot-loop-master.sock:mode=660',
```

Most likely https://github.com/twisted/twisted/pull/12312 is the cause of the regression because it introduces `setTcpNoDelay` to HTTP client. Unfortunately `setTcpNoDelay` is available on all stream sockets, which includes unix domain sockets. Setting TCP NODELAY option on them fails with error.


Discussion:
codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/twisted/twisted/branches/p12tic:12365-http-fix-unix-socket)

### Merging #12366 will **improve performances by 2.95%**

<sub>Comparing <code>p12tic:12365-http-fix-unix-socket</code> (6fc349d) with <code>trunk</code> (a6c0e2d)</sub>



### Summary

`⚡ 1` improvements
`✅ 24` untouched benchmarks




### Benchmarks breakdown

|     | Benchmark | `trunk` | `p12tic:12365-http-fix-unix-socket` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| ⚡ | `test_deferred_await_unfired` | 370.6 µs | 360 µs | +2.95% |

itamarst: Ugggh why is Unix socket a TCP socket :(
adiroiban: Thanks Povilas for the PR.

I left a comment inline.

I don't think that we should call TCP specific methods on an Unix trasport.





adiroiban: So the code at line 2336 is this

https://github.com/twisted/twisted/blob/a6c0e2dd1df7f7610aa4efe64a442db829954e44/src/twisted/web/http.py#L2334-L2337


we need to see why the Unix transport is recognized as a TCP transport
adiroiban: There is someting strange here.

To manualy reproduce

```
$ twist web --listen='unix:/tmp/twisted-web:mode=660'
```

In another terminal trigger a new connection

```
$ echo 'not important' |  socat - UNIX-CONNECT:/tmp/twisted-web
```
adiroiban: More troubleshooting see where we end up with TCP interface for the Unix transport

```
(Pdb) self.transport
<_GenericHTTPChannelProtocol #0 on None>
(Pdb) from zope.interface.declarations import providedBy
(Pdb) ca = providedBy(self.transport)
(Pdb) ca._implied
{classImplements(Server, IUNIXTransport): (), <InterfaceClass twisted.internet.interfaces.IUNIXTransport>: (), classImplements(_SendmsgMixin): (), classImplements(Server): (), classImplements(ServerMixin): (), classImplements(Connection, ITLSTransport, ITCPTransport, ISystemHandle): (), <InterfaceClass twisted.internet.interfaces.ITLSTransport>: (), <InterfaceClass twisted.internet.interfaces.ITCPTransport>: (), <InterfaceClass twisted.internet.interfaces.ISystemHandle>: (), classImplements(ConnectionMixin): (), classImplements(FileDescriptor, IPushProducer, IReadWriteDescriptor, IConsumer, ITransport, IHalfCloseableDescriptor): (), <InterfaceClass twisted.internet.interfaces.IPushProducer>: (), <InterfaceClass twisted.internet.interfaces.IProducer>: (), <InterfaceClass twisted.internet.interfaces.IReadWriteDescriptor>: (), <InterfaceClass twisted.internet.interfaces.IReadDescriptor>: (), <InterfaceClass twisted.internet.interfaces.IWriteDescriptor>: (), <InterfaceClass twisted.internet.interfaces.IFileDescriptor>: (), <InterfaceClass twisted.internet.interfaces.IConsumer>: (), <InterfaceClass twisted.internet.interfaces.ITransport>: (), <InterfaceClass twisted.internet.interfaces.IHalfCloseableDescriptor>: (), classImplements(_ConsumerMixin): (), classImplements(_LogOwner, ILoggingContext): (), <InterfaceClass twisted.internet.interfaces.ILoggingContext>: (), classImplements(_SocketCloser): (), classImplements(_AbortingMixin): (), classImplements(object): (), <InterfaceClass zope.interface.Interface>: ()}
```
adiroiban: I think that the issue is here

`t.internet.unix.Server` inherits from `t.internet.tcp.Server`

https://github.com/twisted/twisted/blob/a6c0e2dd1df7f7610aa4efe64a442db829954e44/src/twisted/internet/unix.py#L241-L245


 which inhertis from `twisted.interent.tcp.Connection`

https://github.com/twisted/twisted/blob/a6c0e2dd1df7f7610aa4efe64a442db829954e44/src/twisted/internet/tcp.py#L769-L773

which implements

https://github.com/twisted/twisted/blob/a6c0e2dd1df7f7610aa4efe64a442db829954e44/src/twisted/internet/tcp.py#L209-L212
itamarst: So maybe better to just force the Unix transport to not implement that interface?
p12tic: I'm not sure if making Unix sockets too different from TCP sockets is the right approach. This would be different than the abstractions encoded in the underlying POSIX API. There TCP and Unix sockets are very similar. For example, compare the socket creation:

```
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
int unix_socket = socket(AF_UNIX, SOCK_STREAM, 0);
```

Unix sockets are treated effectively as just another addressing mode, just like ipv6 (ipv6 socket would be created via `socket(AF_INET6, SOCK_STREAM, 0);`.

The similarity carries over to UDP and datagram UNIX sockets - one just changes SOCK_STREAM to SOCK_DGRAM.

Furthermore, the similarity is enforced by the fact that SO_KEEPALIVE is supported for UNIX sockets as well.

Therefore I think it makes sense to keep classes related to UNIX sockets as a subclasses of classes related to TCP sockets.

Putting abstractions aside, note that setting TCP_NODELAY may fail anyways (see e.g. https://github.com/thespianpy/Thespian/issues/70 or https://github.com/envoyproxy/envoy/issues/1446). so we may end up needing to do `try ... except` anyway. That would result in abstraction changes being not needed. 

itamarst: The possibility for TCP_NODELAY failing makes this a lot more compelling of a workaround, yeah.
adiroiban: One quick and dirty fix is to implement setTcpNoDelay as NOOP for Unix server
glyph: I am inclined to let this slide as an initial fix, given that we shipped something pretty catastrophically broken for a legit use-case, and we should address that before moving on to less significant architectural concerns, but I really don't like the fact that an error occurs here and we just throw it away completely, with no opportunity for the application to recover.

UNIX transports definitely should *not* be implementing an interface called `ITCPTransport`, either. If there are some features we need from it, we should try to address those with a discrete interface.
p12tic: Coverage does not like lines that were changed due to addressing of a pre-existing typing issue. I think it makes sense to ignore this failure for now, because it is completely unrelated to what the PR attempted to fix originally.
p12tic: @glyph @adiroiban @itamarst I think I've addressed your concerns

Patch:
diff --git a/src/twisted/internet/interfaces.py b/src/twisted/internet/interfaces.py
index 6e7d070fc0..28ab9bffbd 100644
--- a/src/twisted/internet/interfaces.py
+++ b/src/twisted/internet/interfaces.py
@@ -787,7 +787,10 @@ class IReactorUNIX(Interface):
     """
 
     def connectUNIX(
-        address: str, factory: "ClientFactory", timeout: float, checkPID: bool
+        address: str,
+        factory: "ClientFactory",
+        timeout: float = 30,
+        checkPID: bool = False,
     ) -> IConnector:
         """
         Connect a client protocol to a UNIX socket.
@@ -804,7 +807,11 @@ class IReactorUNIX(Interface):
         """
 
     def listenUNIX(
-        address: str, factory: "Factory", backlog: int, mode: int, wantPID: bool
+        address: str,
+        factory: "Factory",
+        backlog: int = 50,
+        mode: int = 0o666,
+        wantPID: bool = False,
     ) -> "IListeningPort":
         """
         Listen on a UNIX socket.
diff --git a/src/twisted/internet/tcp.py b/src/twisted/internet/tcp.py
index e0054c1f63..7bc4d1eaac 100644
--- a/src/twisted/internet/tcp.py
+++ b/src/twisted/internet/tcp.py
@@ -337,7 +337,18 @@ class Connection(
         return bool(self.socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
 
     def setTcpNoDelay(self, enabled):
-        self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, enabled)
+        try:
+            # There are bug reports about failures when setting TCP_NODELAY under certain conditions
+            # on macOS: https://github.com/thespianpy/Thespian/issues/70,
+            # https://github.com/envoyproxy/envoy/issues/1446.
+            #
+            # It is reasonable to simply eat errors coming from setting TCP_NODELAY because
+            # TCP_NODELAY is relatively small performance optimization. In almost all cases the
+            # caller will not be able to do anything to remedy the situation and will simply
+            # continue.
+            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, enabled)
+        except OSError as e:  # pragma: no cover
+            log.err(e, "got error when setting TCP_NODELAY on TCP socket")
 
     def getTcpKeepAlive(self):
         return bool(self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE))
diff --git a/src/twisted/internet/unix.py b/src/twisted/internet/unix.py
index c3fe62b22d..b33ef29974 100644
--- a/src/twisted/internet/unix.py
+++ b/src/twisted/internet/unix.py
@@ -294,6 +294,21 @@ class Server(_SendmsgMixin, tcp.Server):
     def getPeer(self):
         return address.UNIXAddress(self.hostname or None)
 
+    def getTcpNoDelay(self):
+        """
+        FIXME:https://github.com/twisted/twisted/issues/12369
+
+        L{twisted.internet.unix.Server} inherits from L{twisted.internet.tcp.Server} which has
+        this method implemented for TCP. For Unix socket, this is just a NOOP to avoid
+        errors for the code that calls TCP specicific methods thinking that the Unix transport
+        is a TCP transport.
+        """
+        return False
+
+    def setTcpNoDelay(self, enabled):
+        # This is not supported on UNIX sockets and therefore silently ignored.
+        pass
+
 
 def _inFilesystemNamespace(path):
     """
@@ -467,6 +482,21 @@ class Client(_SendmsgMixin, tcp.BaseClient):
     def getHost(self):
         return address.UNIXAddress(None)
 
+    def getTcpNoDelay(self):
+        """
+        FIXME:https://github.com/twisted/twisted/issues/12369
+
+        L{twisted.internet.unix.Client} inherits from L{twisted.internet.tcp.Client} which has
+        this method implemented for TCP. For Unix socket, this is just a NOOP to avoid
+        errors for the code that calls TCP specicific methods thinking that the Unix transport
+        is a TCP transport.
+        """
+        return False
+
+    def setTcpNoDelay(self, enabled):
+        # This is not supported on UNIX sockets and therefore silently ignored.
+        pass
+
 
 class Connector(base.BaseConnector):
     def __init__(self, address, factory, timeout, reactor, checkPID):
diff --git a/src/twisted/newsfragments/12365.bugfix b/src/twisted/newsfragments/12365.bugfix
new file mode 100644
index 0000000000..9903674882
--- /dev/null
+++ b/src/twisted/newsfragments/12365.bugfix
@@ -0,0 +1,2 @@
+HTTP server can handle listening on filesystem UNIX sockets.
+This was a regression introduced in 24.10.0.
diff --git a/src/twisted/newsfragments/12372.bugfix b/src/twisted/newsfragments/12372.bugfix
new file mode 100644
index 0000000000..1f6e1f28ed
--- /dev/null
+++ b/src/twisted/newsfragments/12372.bugfix
@@ -0,0 +1,1 @@
+twisted.internet.tcp.Connection.setTcpNoDelay was updated to fix a potential failure when setting TCP_NODELAY on macOS.


Test patch:
diff --git a/src/twisted/internet/test/test_tcp.py b/src/twisted/internet/test/test_tcp.py
index abe06d0a1b..cd2599c6a2 100644
--- a/src/twisted/internet/test/test_tcp.py
+++ b/src/twisted/internet/test/test_tcp.py
@@ -890,7 +890,7 @@ class TCPConnectorTestsBuilder(ReactorBuilder):
         protocolMadeAndClosed = []
 
         def reconnectFailed(ignored):
-            p = clientFactory.protocol
+            p = clientFactory.lastProtocol
             protocolMadeAndClosed.append((p.made, p.closed))
             reactor.stop()
 
diff --git a/src/twisted/internet/test/test_unix.py b/src/twisted/internet/test/test_unix.py
index 3ff2aed95b..7bac2475fa 100644
--- a/src/twisted/internet/test/test_unix.py
+++ b/src/twisted/internet/test/test_unix.py
@@ -13,7 +13,7 @@ from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, socket
 from stat import S_IMODE
 from struct import pack
 from tempfile import mkstemp, mktemp
-from typing import Optional, Sequence, Type
+from typing import Any, Callable, Optional, Sequence, Type
 from unittest import skipIf
 
 try:
@@ -25,7 +25,7 @@ else:
 
 from zope.interface import Interface, implementer
 
-from twisted.internet import base, interfaces
+from twisted.internet import base, error, interfaces, reactor, tcp
 from twisted.internet.address import UNIXAddress
 from twisted.internet.defer import Deferred, fail, gatherResults
 from twisted.internet.endpoints import UNIXClientEndpoint, UNIXServerEndpoint
@@ -51,6 +51,7 @@ from twisted.internet.test.connectionmixins import (
 )
 from twisted.internet.test.reactormixins import ReactorBuilder
 from twisted.internet.test.test_tcp import (
+    ClosingFactory,
     MyClientFactory,
     MyServerFactory,
     StreamTransportTestsMixin,
@@ -61,7 +62,9 @@ from twisted.python.failure import Failure
 from twisted.python.filepath import _coerceToFilesystemEncoding
 from twisted.python.log import addObserver, err, removeObserver
 from twisted.python.reflect import requireModule
-from twisted.python.runtime import platform
+from twisted.python.runtime import platform, platformType
+from twisted.test.test_tcp import AccumulatingProtocol
+from twisted.trial.unittest import TestCase
 
 sendmsg = requireModule("twisted.python.sendmsg")
 sendmsgSkipReason = ""
@@ -1014,3 +1017,119 @@ class UnixClientTestsBuilder(ReactorBuilder, StreamClientTestsMixin):
 
 
 globals().update(UnixClientTestsBuilder.makeTestCaseClasses())
+
+
+@skipIf(platformType != "posix", "UNIX sockets only supported on POSIX.")
+class RealSocketTests(TestCase):
+    """
+    Test real UNIX socket connections.
+    """
+
+    path = "unixpath"
+
+    async def test_closePortInProtocolFactory(self):
+        """
+        A port created with L{IReactorTCP.listenUNIX} can be connected to with
+        L{IReactorTCP.connectUNIX}.
+        """
+        assert interfaces.IReactorUNIX.providedBy(reactor)
+
+        f = ClosingFactory()
+        port = reactor.listenUNIX(self.path, f)
+        assert isinstance(port, tcp.Port)
+        f.port = port
+        self.addCleanup(f.cleanUp)
+        clientF = MyClientFactory()
+        reactor.connectUNIX(self.path, clientF)
+
+        await clientF.deferred
+
+        assert clientF.lastProtocol
+        self.assertTrue(clientF.lastProtocol.made)
+        self.assertTrue(port.disconnected)
+        clientF.lostReason.trap(error.ConnectionDone)
+
+    async def _connectedClientAndServerTest(
+        self, callback: Callable[[AccumulatingProtocol, AccumulatingProtocol], None]
+    ) -> None:
+        """
+        Invoke the given callback with a client protocol and a server protocol
+        which have been connected to each other.
+        """
+        assert interfaces.IReactorUNIX.providedBy(reactor)
+
+        serverFactory = MyServerFactory()
+        serverConnMade: Deferred[AccumulatingProtocol] = Deferred()
+        serverFactory.protocolConnectionMade = serverConnMade
+        port = reactor.listenUNIX(self.path, serverFactory)
+        self.addCleanup(port.stopListening)
+
+        clientF = MyClientFactory()
+        clientConnMade: Deferred[Any] = Deferred()
+        clientF.protocolConnectionMade = clientConnMade
+        reactor.connectUNIX(self.path, clientF)
+
+        serverProtocol, clientProtocol = await gatherResults(
+            [serverConnMade, clientConnMade]
+        )
+
+        callback(serverProtocol, clientProtocol)
+        assert serverProtocol.transport
+        assert clientProtocol.transport
+        serverProtocol.transport.loseConnection()
+        clientProtocol.transport.loseConnection()
+
+    async def test_tcpNoDelay(self):
+        """
+        The transport of a protocol connected with L{IReactorTCP.connectUNIX} or
+        L{IReactor.TCP.listenUNIX} can have L{ITCPTransport.getTcpNoDelay} or
+        L{ITCPTransport.setTcpNoDelay} called without error.
+        """
+
+        def check(
+            serverProtocol: AccumulatingProtocol, clientProtocol: AccumulatingProtocol
+        ) -> None:
+            for p in [serverProtocol, clientProtocol]:
+                assert interfaces.ITCPTransport.providedBy(p.transport)
+                transport = p.transport
+                self.assertEqual(transport.getTcpNoDelay(), 0)
+                transport.setTcpNoDelay(True)
+                self.assertEqual(transport.getTcpNoDelay(), 0)
+                transport.setTcpNoDelay(False)
+                self.assertEqual(transport.getTcpNoDelay(), 0)
+
+        await self._connectedClientAndServerTest(check)
+
+    async def test_tcpKeepAlive(self):
+        """
+        The transport of a protocol connected with L{IReactorTCP.connectUNIX} or
+        L{IReactor.TCP.listenUNIX} can have its I{SO_KEEPALIVE} state inspected
+        and manipulated with L{ITCPTransport.getTcpKeepAlive} and
+        L{ITCPTransport.setTcpKeepAlive}.
+        """
+
+        def check(
+            serverProtocol: AccumulatingProtocol, clientProtocol: AccumulatingProtocol
+        ) -> None:
+            for p in [serverProtocol, clientProtocol]:
+                assert interfaces.ITCPTransport.providedBy(p.transport)
+                transport = p.transport
+                self.assertEqual(transport.getTcpKeepAlive(), False)
+                transport.setTcpKeepAlive(True)
+                self.assertEqual(transport.getTcpKeepAlive(), True)
+                transport.setTcpKeepAlive(False)
+                self.assertEqual(transport.getTcpKeepAlive(), False)
+
+        await self._connectedClientAndServerTest(check)
+
+    async def test_Failing(self):
+        """
+        Tests whether attempting to establish connection to non-existing UNIX path fails
+        """
+        assert interfaces.IReactorUNIX.providedBy(reactor)
+
+        clientF = MyClientFactory()
+        reactor.connectUNIX("non-existing", clientF, timeout=5)
+
+        await clientF.failDeferred
+        clientF.reason.trap(error.ConnectError)
diff --git a/src/twisted/test/test_tcp.py b/src/twisted/test/test_tcp.py
index 91114da6e8..1d70622825 100644
--- a/src/twisted/test/test_tcp.py
+++ b/src/twisted/test/test_tcp.py
@@ -10,7 +10,7 @@ import errno
 import random
 import socket
 from functools import wraps
-from typing import Callable, Optional
+from typing import Optional
 from unittest import skipIf
 
 from zope.interface import implementer
@@ -20,7 +20,6 @@ import hamcrest
 from twisted.internet import defer, error, interfaces, protocol, reactor
 from twisted.internet.address import IPv4Address
 from twisted.internet.interfaces import IHalfCloseableProtocol, IPullProducer
-from twisted.internet.protocol import Protocol
 from twisted.internet.testing import AccumulatingProtocol
 from twisted.protocols import policies
 from twisted.python.log import err, msg
@@ -72,6 +71,7 @@ class ClosingFactory(protocol.ServerFactory):
     """
 
     _cleanerUpper = None
+    port: Optional[interfaces.IListeningPort] = None
 
     def buildProtocol(self, conn):
         self._cleanerUpper = self.port.stopListening()
@@ -81,7 +81,10 @@ class ClosingFactory(protocol.ServerFactory):
         """
         Clean-up for tests to wait for the port to stop listening.
         """
-        if self._cleanerUpper is None:
+        if self._cleanerUpper is None:  # pragma: no cover
+            # Under normal test operation, the port should be already stopped.. Stop it here in
+            # the case the test execution failed without stopping the port.
+            assert self.port
             return self.port.stopListening()
         return self._cleanerUpper
 
@@ -118,9 +121,9 @@ class MyProtocolFactoryMixin:
 
     protocolFactory = AccumulatingProtocol
 
-    protocolConnectionMade = None
-    protocolConnectionLost = None
-    protocol: Optional[Callable[[], Protocol]] = None
+    protocolConnectionMade: Optional[defer.Deferred[AccumulatingProtocol]] = None
+    protocolConnectionLost: Optional[defer.Deferred[AccumulatingProtocol]] = None
+    lastProtocol: Optional[AccumulatingProtocol] = None
     called = 0
 
     def __init__(self):
@@ -137,7 +140,7 @@ class MyProtocolFactoryMixin:
         p.factory = self
         p.closedDeferred = self.protocolConnectionLost
         self.protocolConnectionLost = None
-        self.protocol = p
+        self.lastProtocol = p
         return p
 
 
@@ -154,6 +157,8 @@ class MyClientFactory(MyProtocolFactoryMixin, protocol.ClientFactory):
 
     failed = 0
     stopped = 0
+    deferred: defer.Deferred[None]
+    failDeferred: defer.Deferred[None]
 
     def __init__(self):
         MyProtocolFactoryMixin.__init__(self)
@@ -391,7 +396,7 @@ class LoopbackTests(TestCase):
         reactor.connectTCP("127.0.0.1", portNumber, clientF)
 
         def check(x):
-            self.assertTrue(clientF.protocol.made)
+            self.assertTrue(clientF.lastProtocol.made)
             self.assertTrue(port.disconnected)
             clientF.lostReason.trap(error.ConnectionDone)
 
@@ -761,18 +766,18 @@ class CannotBindTests(TestCase):
 
         def _conmade(results, cf1):
             d = defer.Deferred()
-            cf1.protocol.connectionMade = self._fireWhenDoneFunc(
-                d, cf1.protocol.connectionMade
+            cf1.lastProtocol.connectionMade = self._fireWhenDoneFunc(
+                d, cf1.lastProtocol.connectionMade
             )
             d.addCallback(_check1connect2, cf1)
             return d
 
         def _check1connect2(results, cf1):
-            self.assertEqual(cf1.protocol.made, 1)
+            self.assertEqual(cf1.lastProtocol.made, 1)
 
             d1 = defer.Deferred()
             d2 = defer.Deferred()
-            port = cf1.protocol.transport.getHost().port
+            port = cf1.lastProtocol.transport.getHost().port
             cf2 = MyClientFactory()
             cf2.clientConnectionFailed = self._fireWhenDoneFunc(
                 d1, cf2.clientConnectionFailed
@@ -801,7 +806,7 @@ class CannotBindTests(TestCase):
             d = defer.Deferred()
             d.addCallback(_check1cleanup, cf1)
             cf1.stopFactory = self._fireWhenDoneFunc(d, cf1.stopFactory)
-            cf1.protocol.transport.loseConnection()
+            cf1.lastProtocol.transport.loseConnection()
             return d
 
         def _check1cleanup(results, cf1):
@@ -811,13 +816,6 @@ class CannotBindTests(TestCase):
         return theDeferred
 
 
-class MyOtherClientFactory(protocol.ClientFactory):
-    def buildProtocol(self, address):
-        self.address = address
-        self.protocol = AccumulatingProtocol()
-        return self.protocol
-
-
 class LocalRemoteAddressTests(TestCase):
     """
     Tests for correct getHost/getPeer values and that the correct address is
@@ -842,7 +840,9 @@ class LocalRemoteAddressTests(TestCase):
 
         def check(ignored):
             self.assertEqual([port.getHost()], clientFactory.peerAddresses)
-            self.assertEqual(port.getHost(), clientFactory.protocol.transport.getPeer())
+            self.assertEqual(
+                port.getHost(), clientFactory.lastProtocol.transport.getPeer()
+            )
 
         onConnection.addCallback(check)
 
@@ -897,7 +897,7 @@ class WriterClientFactory(protocol.ClientFactory):
     def buildProtocol(self, addr):
         p = ReaderProtocol()
         p.factory = self
-        self.protocol = p
+        self.lastProtocol = p
         return p
 
 
@@ -1383,8 +1383,8 @@ class AddressTests(TestCase):
         transport of the server protocol and as the C{getPeer} method of the
         transport of the client protocol.
         """
-        serverHost = self.server.protocol.transport.getHost()
-        clientPeer = self.client.protocol.transport.getPeer()
+        serverHost = self.server.lastProtocol.transport.getHost()
+        clientPeer = self.client.lastProtocol.transport.getPeer()
 
         self.assertEqual(
             self.clientWrapper.addresses,
@@ -1429,7 +1429,7 @@ class LargeBufferReaderClientFactory(protocol.ClientFactory):
     def buildProtocol(self, addr):
         p = LargeBufferReaderProtocol()
         p.factory = self
-        self.protocol = p
+        self.lastProtocol = p
         return p
 
 
@@ -1515,7 +1515,7 @@ class MyHCFactory(protocol.ServerFactory):
         self.called += 1
         p = MyHCProtocol()
         p.factory = self
-        self.protocol = p
+        self.lastProtocol = p
         return p
 
 
@@ -1539,7 +1539,7 @@ class HalfCloseTests(TestCase):
         self.clientProtoConnectionLost = self.client.closedDeferred = defer.Deferred()
         self.assertEqual(self.client.transport.connected, 1)
         # Wait for the server to notice there is a connection, too.
-        return loopUntil(lambda: getattr(self.f, "protocol", None) is not None)
+        return loopUntil(lambda: getattr(self.f, "lastProtocol", None) is not None)
 
     def tearDown(self):
         self.assertEqual(self.client.closed, 0)
@@ -1553,16 +1553,16 @@ class HalfCloseTests(TestCase):
         self.assertEqual(self.client.closed, 1)
         # because we did half-close, the server also needs to
         # closed explicitly.
-        self.assertEqual(self.f.protocol.closed, 0)
+        self.assertEqual(self.f.lastProtocol.closed, 0)
         d = defer.Deferred()
 
         def _connectionLost(reason):
-            self.f.protocol.closed = 1
+            self.f.lastProtocol.closed = 1
             d.callback(None)
 
-        self.f.protocol.connectionLost = _connectionLost
-        self.f.protocol.transport.loseConnection()
-        d.addCallback(lambda x: self.assertEqual(self.f.protocol.closed, 1))
+        self.f.lastProtocol.connectionLost = _connectionLost
+        self.f.lastProtocol.transport.loseConnection()
+        d.addCallback(lambda x: self.assertEqual(self.f.lastProtocol.closed, 1))
         return d
 
     def testCloseWriteCloser(self):
@@ -1581,30 +1581,30 @@ class HalfCloseTests(TestCase):
             self.assertFalse(client.closed)
             self.assertTrue(client.writeHalfClosed)
             self.assertFalse(client.readHalfClosed)
-            return loopUntil(lambda: f.protocol.readHalfClosed)
+            return loopUntil(lambda: f.lastProtocol.readHalfClosed)
 
         def write(ignored):
             w = client.transport.write
             w(b" world")
             w(b"lalala fooled you")
             self.assertEqual(0, len(client.transport._tempDataBuffer))
-            self.assertEqual(f.protocol.data, b"hello")
-            self.assertFalse(f.protocol.closed)
-            self.assertTrue(f.protocol.readHalfClosed)
+            self.assertEqual(f.lastProtocol.data, b"hello")
+            self.assertFalse(f.lastProtocol.closed)
+            self.assertTrue(f.lastProtocol.readHalfClosed)
 
         return d.addCallback(loseWrite).addCallback(check).addCallback(write)
 
     def testWriteCloseNotification(self):
         f = self.f
-        f.protocol.transport.loseWriteConnection()
+        f.lastProtocol.transport.loseWriteConnection()
 
         d = defer.gatherResults(
             [
-                loopUntil(lambda: f.protocol.writeHalfClosed),
+                loopUntil(lambda: f.lastProtocol.writeHalfClosed),
                 loopUntil(lambda: self.client.readHalfClosed),
             ]
         )
-        d.addCallback(lambda _: self.assertEqual(f.protocol.readHalfClosed, False))
+        d.addCallback(lambda _: self.assertEqual(f.lastProtocol.readHalfClosed, False))
         return d
 
 
@@ -1641,10 +1641,10 @@ class HalfCloseNoNotificationAndShutdownExceptionTests(TestCase):
         """
         self.client.transport.write(b"hello")
         self.client.transport.loseWriteConnection()
-        self.f.protocol.closedDeferred = d = defer.Deferred()
+        self.f.lastProtocol.closedDeferred = d = defer.Deferred()
         self.client.closedDeferred = d2 = defer.Deferred()
-        d.addCallback(lambda x: self.assertEqual(self.f.protocol.data, b"hello"))
-        d.addCallback(lambda x: self.assertTrue(self.f.protocol.closed))
+        d.addCallback(lambda x: self.assertEqual(self.f.lastProtocol.data, b"hello"))
+        d.addCallback(lambda x: self.assertTrue(self.f.lastProtocol.closed))
         return defer.gatherResults([d, d2])
 
     def testShutdownException(self):
@@ -1652,12 +1652,12 @@ class HalfCloseNoNotificationAndShutdownExceptionTests(TestCase):
         If the other side has already closed its connection,
         loseWriteConnection should pass silently.
         """
-        self.f.protocol.transport.loseConnection()
+        self.f.lastProtocol.transport.loseConnection()
         self.client.transport.write(b"X")
         self.client.transport.loseWriteConnection()
-        self.f.protocol.closedDeferred = d = defer.Deferred()
+        self.f.lastProtocol.closedDeferred = d = defer.Deferred()
         self.client.closedDeferred = d2 = defer.Deferred()
-        d.addCallback(lambda x: self.assertTrue(self.f.protocol.closed))
+        d.addCallback(lambda x: self.assertTrue(self.f.lastProtocol.closed))
         return defer.gatherResults([d, d2])
 
 
@@ -1715,7 +1715,7 @@ class HalfCloseBuggyApplicationTests(TestCase):
         calls it to notify the protocol of that event, the exception should
         be logged and the protocol should be disconnected completely.
         """
-        self.serverFactory.protocol.readConnectionLost = self.aBug
+        self.serverFactory.lastProtocol.readConnectionLost = self.aBug
         return self._notificationRaisesTest()
 
     def test_writeNotificationRaises(self):
@@ -1827,7 +1827,7 @@ class CallBackOrderTests(TestCase):
             """
             protocol.connectionMade callback
             """
-            reactor.callLater(0, client.protocol.transport.loseConnection)
+            reactor.callLater(0, client.lastProtocol.transport.loseConnection)
 
         client.protocolConnectionMade.addCallback(_cbCM)