0
0
Fork 1
mirror of https://mau.dev/maunium/synapse.git synced 2024-12-14 16:53:53 +01:00

Experimental Unix socket support (#15353)

* Add IReactorUNIX to ISynapseReactor type hint.

* Create listen_unix().

Two options, 'path' to the file and 'mode' of permissions(not umask, recommend 666 as default as
nginx/other reverse proxies write to it and it's setup as user www-data)

For the moment, leave the option to always create a PID lockfile turned on by default

* Create UnixListenerConfig and wire it up.

Rename ListenerConfig to TCPListenerConfig, then Union them together into ListenerConfig.
This spidered around a bit, but I think I got it all. Metrics and manhole have been placed
behind a conditional in case of accidental putting them onto a unix socket.

Use new helpers to get if a listener is configured for TLS, and to help create a site tag
for logging.

There are 2 TODO things in parse_listener_def() to finish up at a later point.

* Refactor SynapseRequest to handle logging correctly when using a unix socket.

This prevents an exception when an IP address can not be retrieved for a request.

* Make the 'Synapse now listening on Unix socket' log line a little prettier.

* No silent failures on generic workers when trying to use a unix socket with metrics or manhole.

* Inline variables in app/_base.py

* Update docstring for listen_unix() to remove reference to a hardcoded permission of 0o666 and add a few comments saying where the default IS declared.

* Disallow both a unix socket and a ip/port combo on the same listener resource

* Linting

* Changelog

* review: simplify how listen_unix returns(and get rid of a type: ignore)

* review: fix typo from ConfigError in app/homeserver.py

* review: roll conditional for http_options.tag into get_site_tag() helper(and add docstring)

* review: enhance the conditionals for checking if a port or path is valid, remove a TODO line

* review: Try updating comment in get_client_ip_if_available to clarify what is being retrieved and why

* Pretty up how 'Synapse now listening on Unix Socket' looks by decoding the byte string.

* review: In parse_listener_def(), raise ConfigError if neither socket_path nor port is declared(and fix a typo)
This commit is contained in:
Jason Little 2023-04-03 04:27:51 -05:00 committed by GitHub
parent 9b2ab506c5
commit 56efa9b167
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 239 additions and 90 deletions

1
changelog.d/15353.misc Normal file
View file

@ -0,0 +1 @@
Add experimental support for Unix sockets. Contributed by Jason Little.

View file

@ -41,7 +41,12 @@ from typing_extensions import ParamSpec
import twisted import twisted
from twisted.internet import defer, error, reactor as _reactor from twisted.internet import defer, error, reactor as _reactor
from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorSSL, IReactorTCP from twisted.internet.interfaces import (
IOpenSSLContextFactory,
IReactorSSL,
IReactorTCP,
IReactorUNIX,
)
from twisted.internet.protocol import ServerFactory from twisted.internet.protocol import ServerFactory
from twisted.internet.tcp import Port from twisted.internet.tcp import Port
from twisted.logger import LoggingFile, LogLevel from twisted.logger import LoggingFile, LogLevel
@ -56,7 +61,7 @@ from synapse.app.phone_stats_home import start_phone_stats_home
from synapse.config import ConfigError from synapse.config import ConfigError
from synapse.config._base import format_config_error from synapse.config._base import format_config_error
from synapse.config.homeserver import HomeServerConfig from synapse.config.homeserver import HomeServerConfig
from synapse.config.server import ListenerConfig, ManholeConfig from synapse.config.server import ListenerConfig, ManholeConfig, TCPListenerConfig
from synapse.crypto import context_factory from synapse.crypto import context_factory
from synapse.events.presence_router import load_legacy_presence_router from synapse.events.presence_router import load_legacy_presence_router
from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.events.spamcheck import load_legacy_spam_checkers
@ -351,6 +356,28 @@ def listen_tcp(
return r # type: ignore[return-value] return r # type: ignore[return-value]
def listen_unix(
path: str,
mode: int,
factory: ServerFactory,
reactor: IReactorUNIX = reactor,
backlog: int = 50,
) -> List[Port]:
"""
Create a UNIX socket for a given path and 'mode' permission
Returns:
list of twisted.internet.tcp.Port listening for TCP connections
"""
wantPID = True
return [
# IReactorUNIX returns an object implementing IListeningPort from listenUNIX,
# but we know it will be a Port instance.
cast(Port, reactor.listenUNIX(path, factory, backlog, mode, wantPID))
]
def listen_http( def listen_http(
listener_config: ListenerConfig, listener_config: ListenerConfig,
root_resource: Resource, root_resource: Resource,
@ -359,18 +386,13 @@ def listen_http(
context_factory: Optional[IOpenSSLContextFactory], context_factory: Optional[IOpenSSLContextFactory],
reactor: ISynapseReactor = reactor, reactor: ISynapseReactor = reactor,
) -> List[Port]: ) -> List[Port]:
port = listener_config.port
bind_addresses = listener_config.bind_addresses
tls = listener_config.tls
assert listener_config.http_options is not None assert listener_config.http_options is not None
site_tag = listener_config.http_options.tag site_tag = listener_config.get_site_tag()
if site_tag is None:
site_tag = str(port)
site = SynapseSite( site = SynapseSite(
"synapse.access.%s.%s" % ("https" if tls else "http", site_tag), "synapse.access.%s.%s"
% ("https" if listener_config.is_tls() else "http", site_tag),
site_tag, site_tag,
listener_config, listener_config,
root_resource, root_resource,
@ -378,25 +400,41 @@ def listen_http(
max_request_body_size=max_request_body_size, max_request_body_size=max_request_body_size,
reactor=reactor, reactor=reactor,
) )
if tls:
if isinstance(listener_config, TCPListenerConfig):
if listener_config.is_tls():
# refresh_certificate should have been called before this. # refresh_certificate should have been called before this.
assert context_factory is not None assert context_factory is not None
ports = listen_ssl( ports = listen_ssl(
bind_addresses, listener_config.bind_addresses,
port, listener_config.port,
site, site,
context_factory, context_factory,
reactor=reactor, reactor=reactor,
) )
logger.info("Synapse now listening on TCP port %d (TLS)", port) logger.info(
"Synapse now listening on TCP port %d (TLS)", listener_config.port
)
else: else:
ports = listen_tcp( ports = listen_tcp(
bind_addresses, listener_config.bind_addresses,
port, listener_config.port,
site, site,
reactor=reactor, reactor=reactor,
) )
logger.info("Synapse now listening on TCP port %d", port) logger.info("Synapse now listening on TCP port %d", listener_config.port)
else:
ports = listen_unix(
listener_config.path, listener_config.mode, site, reactor=reactor
)
# getHost() returns a UNIXAddress which contains an instance variable of 'name'
# encoded as a byte string. Decode as utf-8 so pretty.
logger.info(
"Synapse now listening on Unix Socket at: "
f"{ports[0].getHost().name.decode('utf-8')}"
)
return ports return ports

View file

@ -38,7 +38,7 @@ from synapse.app._base import (
from synapse.config._base import ConfigError from synapse.config._base import ConfigError
from synapse.config.homeserver import HomeServerConfig from synapse.config.homeserver import HomeServerConfig
from synapse.config.logger import setup_logging from synapse.config.logger import setup_logging
from synapse.config.server import ListenerConfig from synapse.config.server import ListenerConfig, TCPListenerConfig
from synapse.federation.transport.server import TransportLayerServer from synapse.federation.transport.server import TransportLayerServer
from synapse.http.server import JsonResource, OptionsResource from synapse.http.server import JsonResource, OptionsResource
from synapse.logging.context import LoggingContext from synapse.logging.context import LoggingContext
@ -236,12 +236,18 @@ class GenericWorkerServer(HomeServer):
if listener.type == "http": if listener.type == "http":
self._listen_http(listener) self._listen_http(listener)
elif listener.type == "manhole": elif listener.type == "manhole":
if isinstance(listener, TCPListenerConfig):
_base.listen_manhole( _base.listen_manhole(
listener.bind_addresses, listener.bind_addresses,
listener.port, listener.port,
manhole_settings=self.config.server.manhole_settings, manhole_settings=self.config.server.manhole_settings,
manhole_globals={"hs": self}, manhole_globals={"hs": self},
) )
else:
raise ConfigError(
"Can not using a unix socket for manhole at this time."
)
elif listener.type == "metrics": elif listener.type == "metrics":
if not self.config.metrics.enable_metrics: if not self.config.metrics.enable_metrics:
logger.warning( logger.warning(
@ -249,10 +255,16 @@ class GenericWorkerServer(HomeServer):
"enable_metrics is not True!" "enable_metrics is not True!"
) )
else: else:
if isinstance(listener, TCPListenerConfig):
_base.listen_metrics( _base.listen_metrics(
listener.bind_addresses, listener.bind_addresses,
listener.port, listener.port,
) )
else:
raise ConfigError(
"Can not use a unix socket for metrics at this time."
)
else: else:
logger.warning("Unsupported listener type: %s", listener.type) logger.warning("Unsupported listener type: %s", listener.type)

View file

@ -44,7 +44,7 @@ from synapse.app._base import (
) )
from synapse.config._base import ConfigError, format_config_error from synapse.config._base import ConfigError, format_config_error
from synapse.config.homeserver import HomeServerConfig from synapse.config.homeserver import HomeServerConfig
from synapse.config.server import ListenerConfig from synapse.config.server import ListenerConfig, TCPListenerConfig
from synapse.federation.transport.server import TransportLayerServer from synapse.federation.transport.server import TransportLayerServer
from synapse.http.additional_resource import AdditionalResource from synapse.http.additional_resource import AdditionalResource
from synapse.http.server import ( from synapse.http.server import (
@ -78,14 +78,13 @@ class SynapseHomeServer(HomeServer):
DATASTORE_CLASS = DataStore # type: ignore DATASTORE_CLASS = DataStore # type: ignore
def _listener_http( def _listener_http(
self, config: HomeServerConfig, listener_config: ListenerConfig self,
config: HomeServerConfig,
listener_config: ListenerConfig,
) -> Iterable[Port]: ) -> Iterable[Port]:
port = listener_config.port
# Must exist since this is an HTTP listener. # Must exist since this is an HTTP listener.
assert listener_config.http_options is not None assert listener_config.http_options is not None
site_tag = listener_config.http_options.tag site_tag = listener_config.get_site_tag()
if site_tag is None:
site_tag = str(port)
# We always include a health resource. # We always include a health resource.
resources: Dict[str, Resource] = {"/health": HealthResource()} resources: Dict[str, Resource] = {"/health": HealthResource()}
@ -252,12 +251,17 @@ class SynapseHomeServer(HomeServer):
self._listener_http(self.config, listener) self._listener_http(self.config, listener)
) )
elif listener.type == "manhole": elif listener.type == "manhole":
if isinstance(listener, TCPListenerConfig):
_base.listen_manhole( _base.listen_manhole(
listener.bind_addresses, listener.bind_addresses,
listener.port, listener.port,
manhole_settings=self.config.server.manhole_settings, manhole_settings=self.config.server.manhole_settings,
manhole_globals={"hs": self}, manhole_globals={"hs": self},
) )
else:
raise ConfigError(
"Can not use a unix socket for manhole at this time."
)
elif listener.type == "metrics": elif listener.type == "metrics":
if not self.config.metrics.enable_metrics: if not self.config.metrics.enable_metrics:
logger.warning( logger.warning(
@ -265,10 +269,16 @@ class SynapseHomeServer(HomeServer):
"enable_metrics is not True!" "enable_metrics is not True!"
) )
else: else:
if isinstance(listener, TCPListenerConfig):
_base.listen_metrics( _base.listen_metrics(
listener.bind_addresses, listener.bind_addresses,
listener.port, listener.port,
) )
else:
raise ConfigError(
"Can not use a unix socket for metrics at this time."
)
else: else:
# this shouldn't happen, as the listener type should have been checked # this shouldn't happen, as the listener type should have been checked
# during parsing # during parsing

View file

@ -214,17 +214,52 @@ class HttpListenerConfig:
@attr.s(slots=True, frozen=True, auto_attribs=True) @attr.s(slots=True, frozen=True, auto_attribs=True)
class ListenerConfig: class TCPListenerConfig:
"""Object describing the configuration of a single listener.""" """Object describing the configuration of a single TCP listener."""
port: int = attr.ib(validator=attr.validators.instance_of(int)) port: int = attr.ib(validator=attr.validators.instance_of(int))
bind_addresses: List[str] bind_addresses: List[str] = attr.ib(validator=attr.validators.instance_of(List))
type: str = attr.ib(validator=attr.validators.in_(KNOWN_LISTENER_TYPES)) type: str = attr.ib(validator=attr.validators.in_(KNOWN_LISTENER_TYPES))
tls: bool = False tls: bool = False
# http_options is only populated if type=http # http_options is only populated if type=http
http_options: Optional[HttpListenerConfig] = None http_options: Optional[HttpListenerConfig] = None
def get_site_tag(self) -> str:
"""Retrieves http_options.tag if it exists, otherwise the port number."""
if self.http_options and self.http_options.tag is not None:
return self.http_options.tag
else:
return str(self.port)
def is_tls(self) -> bool:
return self.tls
@attr.s(slots=True, frozen=True, auto_attribs=True)
class UnixListenerConfig:
"""Object describing the configuration of a single Unix socket listener."""
# Note: unix sockets can not be tls encrypted, so HAVE to be behind a tls-handling
# reverse proxy
path: str = attr.ib()
# A default(0o666) for this is set in parse_listener_def() below
mode: int
type: str = attr.ib(validator=attr.validators.in_(KNOWN_LISTENER_TYPES))
# http_options is only populated if type=http
http_options: Optional[HttpListenerConfig] = None
def get_site_tag(self) -> str:
return "unix"
def is_tls(self) -> bool:
"""Unix sockets can't have TLS"""
return False
ListenerConfig = Union[TCPListenerConfig, UnixListenerConfig]
@attr.s(slots=True, frozen=True, auto_attribs=True) @attr.s(slots=True, frozen=True, auto_attribs=True)
class ManholeConfig: class ManholeConfig:
@ -531,12 +566,12 @@ class ServerConfig(Config):
self.listeners = [parse_listener_def(i, x) for i, x in enumerate(listeners)] self.listeners = [parse_listener_def(i, x) for i, x in enumerate(listeners)]
# no_tls is not really supported any more, but let's grandfather it in # no_tls is not really supported anymore, but let's grandfather it in here.
# here.
if config.get("no_tls", False): if config.get("no_tls", False):
l2 = [] l2 = []
for listener in self.listeners: for listener in self.listeners:
if listener.tls: if isinstance(listener, TCPListenerConfig) and listener.tls:
# Use isinstance() as the assertion this *has* a listener.port
logger.info( logger.info(
"Ignoring TLS-enabled listener on port %i due to no_tls", "Ignoring TLS-enabled listener on port %i due to no_tls",
listener.port, listener.port,
@ -577,7 +612,7 @@ class ServerConfig(Config):
) )
self.listeners.append( self.listeners.append(
ListenerConfig( TCPListenerConfig(
port=bind_port, port=bind_port,
bind_addresses=[bind_host], bind_addresses=[bind_host],
tls=True, tls=True,
@ -589,7 +624,7 @@ class ServerConfig(Config):
unsecure_port = config.get("unsecure_port", bind_port - 400) unsecure_port = config.get("unsecure_port", bind_port - 400)
if unsecure_port: if unsecure_port:
self.listeners.append( self.listeners.append(
ListenerConfig( TCPListenerConfig(
port=unsecure_port, port=unsecure_port,
bind_addresses=[bind_host], bind_addresses=[bind_host],
tls=False, tls=False,
@ -601,7 +636,7 @@ class ServerConfig(Config):
manhole = config.get("manhole") manhole = config.get("manhole")
if manhole: if manhole:
self.listeners.append( self.listeners.append(
ListenerConfig( TCPListenerConfig(
port=manhole, port=manhole,
bind_addresses=["127.0.0.1"], bind_addresses=["127.0.0.1"],
type="manhole", type="manhole",
@ -648,7 +683,7 @@ class ServerConfig(Config):
logger.warning(METRICS_PORT_WARNING) logger.warning(METRICS_PORT_WARNING)
self.listeners.append( self.listeners.append(
ListenerConfig( TCPListenerConfig(
port=metrics_port, port=metrics_port,
bind_addresses=[config.get("metrics_bind_host", "127.0.0.1")], bind_addresses=[config.get("metrics_bind_host", "127.0.0.1")],
type="http", type="http",
@ -724,7 +759,7 @@ class ServerConfig(Config):
self.delete_stale_devices_after = None self.delete_stale_devices_after = None
def has_tls_listener(self) -> bool: def has_tls_listener(self) -> bool:
return any(listener.tls for listener in self.listeners) return any(listener.is_tls() for listener in self.listeners)
def generate_config_section( def generate_config_section(
self, self,
@ -904,11 +939,56 @@ def parse_listener_def(num: int, listener: Any) -> ListenerConfig:
raise ConfigError(DIRECT_TCP_ERROR, ("listeners", str(num), "type")) raise ConfigError(DIRECT_TCP_ERROR, ("listeners", str(num), "type"))
port = listener.get("port") port = listener.get("port")
if type(port) is not int: socket_path = listener.get("path")
# Either a port or a path should be declared at a minimum. Using both would be bad.
if port is not None and not isinstance(port, int):
raise ConfigError("Listener configuration is lacking a valid 'port' option") raise ConfigError("Listener configuration is lacking a valid 'port' option")
if socket_path is not None and not isinstance(socket_path, str):
raise ConfigError("Listener configuration is lacking a valid 'path' option")
if port and socket_path:
raise ConfigError(
"Can not have both a UNIX socket and an IP/port declared for the same "
"resource!"
)
if port is None and socket_path is None:
raise ConfigError(
"Must have either a UNIX socket or an IP/port declared for a given "
"resource!"
)
tls = listener.get("tls", False) tls = listener.get("tls", False)
http_config = None
if listener_type == "http":
try:
resources = [
HttpResourceConfig(**res) for res in listener.get("resources", [])
]
except ValueError as e:
raise ConfigError("Unknown listener resource") from e
# For a unix socket, default x_forwarded to True, as this is the only way of
# getting a client IP.
# Note: a reverse proxy is required anyway, as there is no way of exposing a
# unix socket to the internet.
http_config = HttpListenerConfig(
x_forwarded=listener.get("x_forwarded", (True if socket_path else False)),
resources=resources,
additional_resources=listener.get("additional_resources", {}),
tag=listener.get("tag"),
request_id_header=listener.get("request_id_header"),
experimental_cors_msc3886=listener.get("experimental_cors_msc3886", False),
)
if socket_path:
# TODO: Add in path validation, like if the directory exists and is writable?
# Set a default for the permission, in case it's left out
socket_mode = listener.get("mode", 0o666)
return UnixListenerConfig(socket_path, socket_mode, listener_type, http_config)
else:
assert port is not None
bind_addresses = listener.get("bind_addresses", []) bind_addresses = listener.get("bind_addresses", [])
bind_address = listener.get("bind_address") bind_address = listener.get("bind_address")
# if bind_address was specified, add it to the list of addresses # if bind_address was specified, add it to the list of addresses
@ -923,25 +1003,7 @@ def parse_listener_def(num: int, listener: Any) -> ListenerConfig:
else: else:
bind_addresses.extend(DEFAULT_BIND_ADDRESSES) bind_addresses.extend(DEFAULT_BIND_ADDRESSES)
http_config = None return TCPListenerConfig(port, bind_addresses, listener_type, tls, http_config)
if listener_type == "http":
try:
resources = [
HttpResourceConfig(**res) for res in listener.get("resources", [])
]
except ValueError as e:
raise ConfigError("Unknown listener resource") from e
http_config = HttpListenerConfig(
x_forwarded=listener.get("x_forwarded", False),
resources=resources,
additional_resources=listener.get("additional_resources", {}),
tag=listener.get("tag"),
request_id_header=listener.get("request_id_header"),
experimental_cors_msc3886=listener.get("experimental_cors_msc3886", False),
)
return ListenerConfig(port, bind_addresses, listener_type, tls, http_config)
_MANHOLE_SETTINGS_SCHEMA = { _MANHOLE_SETTINGS_SCHEMA = {

View file

@ -19,15 +19,18 @@ from typing import Any, Dict, List, Union
import attr import attr
from synapse.types import JsonDict from synapse.config._base import (
from ._base import (
Config, Config,
ConfigError, ConfigError,
RoutableShardedWorkerHandlingConfig, RoutableShardedWorkerHandlingConfig,
ShardedWorkerHandlingConfig, ShardedWorkerHandlingConfig,
) )
from .server import DIRECT_TCP_ERROR, ListenerConfig, parse_listener_def from synapse.config.server import (
DIRECT_TCP_ERROR,
TCPListenerConfig,
parse_listener_def,
)
from synapse.types import JsonDict
_DEPRECATED_WORKER_DUTY_OPTION_USED = """ _DEPRECATED_WORKER_DUTY_OPTION_USED = """
The '%s' configuration option is deprecated and will be removed in a future The '%s' configuration option is deprecated and will be removed in a future
@ -161,7 +164,7 @@ class WorkerConfig(Config):
manhole = config.get("worker_manhole") manhole = config.get("worker_manhole")
if manhole: if manhole:
self.worker_listeners.append( self.worker_listeners.append(
ListenerConfig( TCPListenerConfig(
port=manhole, port=manhole,
bind_addresses=["127.0.0.1"], bind_addresses=["127.0.0.1"],
type="manhole", type="manhole",

View file

@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any, Generator, Optional, Tuple, Union
import attr import attr
from zope.interface import implementer from zope.interface import implementer
from twisted.internet.address import UNIXAddress
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IAddress, IReactorTime from twisted.internet.interfaces import IAddress, IReactorTime
from twisted.python.failure import Failure from twisted.python.failure import Failure
@ -257,7 +258,7 @@ class SynapseRequest(Request):
request_id, request_id,
request=ContextRequest( request=ContextRequest(
request_id=request_id, request_id=request_id,
ip_address=self.getClientAddress().host, ip_address=self.get_client_ip_if_available(),
site_tag=self.synapse_site.site_tag, site_tag=self.synapse_site.site_tag,
# The requester is going to be unknown at this point. # The requester is going to be unknown at this point.
requester=None, requester=None,
@ -414,7 +415,7 @@ class SynapseRequest(Request):
self.synapse_site.access_logger.debug( self.synapse_site.access_logger.debug(
"%s - %s - Received request: %s %s", "%s - %s - Received request: %s %s",
self.getClientAddress().host, self.get_client_ip_if_available(),
self.synapse_site.site_tag, self.synapse_site.site_tag,
self.get_method(), self.get_method(),
self.get_redacted_uri(), self.get_redacted_uri(),
@ -462,7 +463,7 @@ class SynapseRequest(Request):
"%s - %s - {%s}" "%s - %s - {%s}"
" Processed request: %.3fsec/%.3fsec (%.3fsec, %.3fsec) (%.3fsec/%.3fsec/%d)" " Processed request: %.3fsec/%.3fsec (%.3fsec, %.3fsec) (%.3fsec/%.3fsec/%d)"
' %sB %s "%s %s %s" "%s" [%d dbevts]', ' %sB %s "%s %s %s" "%s" [%d dbevts]',
self.getClientAddress().host, self.get_client_ip_if_available(),
self.synapse_site.site_tag, self.synapse_site.site_tag,
requester, requester,
processing_time, processing_time,
@ -500,6 +501,26 @@ class SynapseRequest(Request):
return True return True
def get_client_ip_if_available(self) -> str:
"""Logging helper. Return something useful when a client IP is not retrievable
from a unix socket.
In practice, this returns the socket file path on a SynapseRequest if using a
unix socket and the normal IP address for TCP sockets.
"""
# getClientAddress().host returns a proper IP address for a TCP socket. But
# unix sockets have no concept of IP addresses or ports and return a
# UNIXAddress containing a 'None' value. In order to get something usable for
# logs(where this is used) get the unix socket file. getHost() returns a
# UNIXAddress containing a value of the socket file and has an instance
# variable of 'name' encoded as a byte string containing the path we want.
# Decode to utf-8 so it looks nice.
if isinstance(self.getClientAddress(), UNIXAddress):
return self.getHost().name.decode("utf-8")
else:
return self.getClientAddress().host
class XForwardedForRequest(SynapseRequest): class XForwardedForRequest(SynapseRequest):
"""Request object which honours proxy headers """Request object which honours proxy headers

View file

@ -50,6 +50,7 @@ from twisted.internet.interfaces import (
IReactorTCP, IReactorTCP,
IReactorThreads, IReactorThreads,
IReactorTime, IReactorTime,
IReactorUNIX,
) )
from synapse.api.errors import Codes, SynapseError from synapse.api.errors import Codes, SynapseError
@ -91,6 +92,7 @@ StrCollection = Union[Tuple[str, ...], List[str], AbstractSet[str]]
class ISynapseReactor( class ISynapseReactor(
IReactorTCP, IReactorTCP,
IReactorSSL, IReactorSSL,
IReactorUNIX,
IReactorPluggableNameResolver, IReactorPluggableNameResolver,
IReactorTime, IReactorTime,
IReactorCore, IReactorCore,