mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-12-26 14:39:21 -05:00
Add ability to blacklist ip ranges for federation traffic (#5043)
This commit is contained in:
parent
2e1129b5f7
commit
5a4b328f52
1
changelog.d/5043.feature
Normal file
1
changelog.d/5043.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add ability to blacklist IP ranges for the federation client.
|
@ -115,6 +115,24 @@ pid_file: DATADIR/homeserver.pid
|
|||||||
# - nyc.example.com
|
# - nyc.example.com
|
||||||
# - syd.example.com
|
# - syd.example.com
|
||||||
|
|
||||||
|
# Prevent federation requests from being sent to the following
|
||||||
|
# blacklist IP address CIDR ranges. If this option is not specified, or
|
||||||
|
# specified with an empty list, no ip range blacklist will be enforced.
|
||||||
|
#
|
||||||
|
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
||||||
|
# listed here, since they correspond to unroutable addresses.)
|
||||||
|
#
|
||||||
|
federation_ip_range_blacklist:
|
||||||
|
- '127.0.0.0/8'
|
||||||
|
- '10.0.0.0/8'
|
||||||
|
- '172.16.0.0/12'
|
||||||
|
- '192.168.0.0/16'
|
||||||
|
- '100.64.0.0/10'
|
||||||
|
- '169.254.0.0/16'
|
||||||
|
- '::1/128'
|
||||||
|
- 'fe80::/64'
|
||||||
|
- 'fc00::/7'
|
||||||
|
|
||||||
# List of ports that Synapse should listen on, their purpose and their
|
# List of ports that Synapse should listen on, their purpose and their
|
||||||
# configuration.
|
# configuration.
|
||||||
#
|
#
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
from netaddr import IPSet
|
||||||
|
|
||||||
from synapse.http.endpoint import parse_and_validate_server_name
|
from synapse.http.endpoint import parse_and_validate_server_name
|
||||||
from synapse.python_dependencies import DependencyException, check_requirements
|
from synapse.python_dependencies import DependencyException, check_requirements
|
||||||
|
|
||||||
@ -137,6 +139,24 @@ class ServerConfig(Config):
|
|||||||
for domain in federation_domain_whitelist:
|
for domain in federation_domain_whitelist:
|
||||||
self.federation_domain_whitelist[domain] = True
|
self.federation_domain_whitelist[domain] = True
|
||||||
|
|
||||||
|
self.federation_ip_range_blacklist = config.get(
|
||||||
|
"federation_ip_range_blacklist", [],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to create an IPSet from the given ranges
|
||||||
|
try:
|
||||||
|
self.federation_ip_range_blacklist = IPSet(
|
||||||
|
self.federation_ip_range_blacklist
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always blacklist 0.0.0.0, ::
|
||||||
|
self.federation_ip_range_blacklist.update(["0.0.0.0", "::"])
|
||||||
|
except Exception as e:
|
||||||
|
raise ConfigError(
|
||||||
|
"Invalid range(s) provided in "
|
||||||
|
"federation_ip_range_blacklist: %s" % e
|
||||||
|
)
|
||||||
|
|
||||||
if self.public_baseurl is not None:
|
if self.public_baseurl is not None:
|
||||||
if self.public_baseurl[-1] != '/':
|
if self.public_baseurl[-1] != '/':
|
||||||
self.public_baseurl += '/'
|
self.public_baseurl += '/'
|
||||||
@ -386,6 +406,24 @@ class ServerConfig(Config):
|
|||||||
# - nyc.example.com
|
# - nyc.example.com
|
||||||
# - syd.example.com
|
# - syd.example.com
|
||||||
|
|
||||||
|
# Prevent federation requests from being sent to the following
|
||||||
|
# blacklist IP address CIDR ranges. If this option is not specified, or
|
||||||
|
# specified with an empty list, no ip range blacklist will be enforced.
|
||||||
|
#
|
||||||
|
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
|
||||||
|
# listed here, since they correspond to unroutable addresses.)
|
||||||
|
#
|
||||||
|
federation_ip_range_blacklist:
|
||||||
|
- '127.0.0.0/8'
|
||||||
|
- '10.0.0.0/8'
|
||||||
|
- '172.16.0.0/12'
|
||||||
|
- '192.168.0.0/16'
|
||||||
|
- '100.64.0.0/10'
|
||||||
|
- '169.254.0.0/16'
|
||||||
|
- '::1/128'
|
||||||
|
- 'fe80::/64'
|
||||||
|
- 'fc00::/7'
|
||||||
|
|
||||||
# List of ports that Synapse should listen on, their purpose and their
|
# List of ports that Synapse should listen on, their purpose and their
|
||||||
# configuration.
|
# configuration.
|
||||||
#
|
#
|
||||||
|
@ -165,7 +165,8 @@ class BlacklistingAgentWrapper(Agent):
|
|||||||
ip_address, self._ip_whitelist, self._ip_blacklist
|
ip_address, self._ip_whitelist, self._ip_blacklist
|
||||||
):
|
):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Blocking access to %s because of blacklist" % (ip_address,)
|
"Blocking access to %s due to blacklist" %
|
||||||
|
(ip_address,)
|
||||||
)
|
)
|
||||||
e = SynapseError(403, "IP address blocked by IP blacklist entry")
|
e = SynapseError(403, "IP address blocked by IP blacklist entry")
|
||||||
return defer.fail(Failure(e))
|
return defer.fail(Failure(e))
|
||||||
@ -263,9 +264,6 @@ class SimpleHttpClient(object):
|
|||||||
uri (str): URI to query.
|
uri (str): URI to query.
|
||||||
data (bytes): Data to send in the request body, if applicable.
|
data (bytes): Data to send in the request body, if applicable.
|
||||||
headers (t.w.http_headers.Headers): Request headers.
|
headers (t.w.http_headers.Headers): Request headers.
|
||||||
|
|
||||||
Raises:
|
|
||||||
SynapseError: If the IP is blacklisted.
|
|
||||||
"""
|
"""
|
||||||
# A small wrapper around self.agent.request() so we can easily attach
|
# A small wrapper around self.agent.request() so we can easily attach
|
||||||
# counters to it
|
# counters to it
|
||||||
|
@ -27,9 +27,11 @@ import treq
|
|||||||
from canonicaljson import encode_canonical_json
|
from canonicaljson import encode_canonical_json
|
||||||
from prometheus_client import Counter
|
from prometheus_client import Counter
|
||||||
from signedjson.sign import sign_json
|
from signedjson.sign import sign_json
|
||||||
|
from zope.interface import implementer
|
||||||
|
|
||||||
from twisted.internet import defer, protocol
|
from twisted.internet import defer, protocol
|
||||||
from twisted.internet.error import DNSLookupError
|
from twisted.internet.error import DNSLookupError
|
||||||
|
from twisted.internet.interfaces import IReactorPluggableNameResolver
|
||||||
from twisted.internet.task import _EPSILON, Cooperator
|
from twisted.internet.task import _EPSILON, Cooperator
|
||||||
from twisted.web._newclient import ResponseDone
|
from twisted.web._newclient import ResponseDone
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
@ -44,6 +46,7 @@ from synapse.api.errors import (
|
|||||||
SynapseError,
|
SynapseError,
|
||||||
)
|
)
|
||||||
from synapse.http import QuieterFileBodyProducer
|
from synapse.http import QuieterFileBodyProducer
|
||||||
|
from synapse.http.client import BlacklistingAgentWrapper, IPBlacklistingResolver
|
||||||
from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent
|
from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent
|
||||||
from synapse.util.async_helpers import timeout_deferred
|
from synapse.util.async_helpers import timeout_deferred
|
||||||
from synapse.util.logcontext import make_deferred_yieldable
|
from synapse.util.logcontext import make_deferred_yieldable
|
||||||
@ -172,19 +175,44 @@ class MatrixFederationHttpClient(object):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.signing_key = hs.config.signing_key[0]
|
self.signing_key = hs.config.signing_key[0]
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
reactor = hs.get_reactor()
|
|
||||||
|
real_reactor = hs.get_reactor()
|
||||||
|
|
||||||
|
# We need to use a DNS resolver which filters out blacklisted IP
|
||||||
|
# addresses, to prevent DNS rebinding.
|
||||||
|
nameResolver = IPBlacklistingResolver(
|
||||||
|
real_reactor, None, hs.config.federation_ip_range_blacklist,
|
||||||
|
)
|
||||||
|
|
||||||
|
@implementer(IReactorPluggableNameResolver)
|
||||||
|
class Reactor(object):
|
||||||
|
def __getattr__(_self, attr):
|
||||||
|
if attr == "nameResolver":
|
||||||
|
return nameResolver
|
||||||
|
else:
|
||||||
|
return getattr(real_reactor, attr)
|
||||||
|
|
||||||
|
self.reactor = Reactor()
|
||||||
|
|
||||||
self.agent = MatrixFederationAgent(
|
self.agent = MatrixFederationAgent(
|
||||||
hs.get_reactor(),
|
self.reactor,
|
||||||
tls_client_options_factory,
|
tls_client_options_factory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Use a BlacklistingAgentWrapper to prevent circumventing the IP
|
||||||
|
# blacklist via IP literals in server names
|
||||||
|
self.agent = BlacklistingAgentWrapper(
|
||||||
|
self.agent, self.reactor,
|
||||||
|
ip_blacklist=hs.config.federation_ip_range_blacklist,
|
||||||
|
)
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self._store = hs.get_datastore()
|
self._store = hs.get_datastore()
|
||||||
self.version_string_bytes = hs.version_string.encode('ascii')
|
self.version_string_bytes = hs.version_string.encode('ascii')
|
||||||
self.default_timeout = 60
|
self.default_timeout = 60
|
||||||
|
|
||||||
def schedule(x):
|
def schedule(x):
|
||||||
reactor.callLater(_EPSILON, x)
|
self.reactor.callLater(_EPSILON, x)
|
||||||
|
|
||||||
self._cooperator = Cooperator(scheduler=schedule)
|
self._cooperator = Cooperator(scheduler=schedule)
|
||||||
|
|
||||||
@ -370,7 +398,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
request_deferred = timeout_deferred(
|
request_deferred = timeout_deferred(
|
||||||
request_deferred,
|
request_deferred,
|
||||||
timeout=_sec_timeout,
|
timeout=_sec_timeout,
|
||||||
reactor=self.hs.get_reactor(),
|
reactor=self.reactor,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = yield request_deferred
|
response = yield request_deferred
|
||||||
@ -397,7 +425,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
d = timeout_deferred(
|
d = timeout_deferred(
|
||||||
d,
|
d,
|
||||||
timeout=_sec_timeout,
|
timeout=_sec_timeout,
|
||||||
reactor=self.hs.get_reactor(),
|
reactor=self.reactor,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -586,7 +614,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
body = yield _handle_json_response(
|
body = yield _handle_json_response(
|
||||||
self.hs.get_reactor(), self.default_timeout, request, response,
|
self.reactor, self.default_timeout, request, response,
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(body)
|
defer.returnValue(body)
|
||||||
@ -645,7 +673,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
_sec_timeout = self.default_timeout
|
_sec_timeout = self.default_timeout
|
||||||
|
|
||||||
body = yield _handle_json_response(
|
body = yield _handle_json_response(
|
||||||
self.hs.get_reactor(), _sec_timeout, request, response,
|
self.reactor, _sec_timeout, request, response,
|
||||||
)
|
)
|
||||||
defer.returnValue(body)
|
defer.returnValue(body)
|
||||||
|
|
||||||
@ -704,7 +732,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
body = yield _handle_json_response(
|
body = yield _handle_json_response(
|
||||||
self.hs.get_reactor(), self.default_timeout, request, response,
|
self.reactor, self.default_timeout, request, response,
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(body)
|
defer.returnValue(body)
|
||||||
@ -753,7 +781,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
body = yield _handle_json_response(
|
body = yield _handle_json_response(
|
||||||
self.hs.get_reactor(), self.default_timeout, request, response,
|
self.reactor, self.default_timeout, request, response,
|
||||||
)
|
)
|
||||||
defer.returnValue(body)
|
defer.returnValue(body)
|
||||||
|
|
||||||
@ -801,7 +829,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
d = _readBodyToFile(response, output_stream, max_size)
|
d = _readBodyToFile(response, output_stream, max_size)
|
||||||
d.addTimeout(self.default_timeout, self.hs.get_reactor())
|
d.addTimeout(self.default_timeout, self.reactor)
|
||||||
length = yield make_deferred_yieldable(d)
|
length = yield make_deferred_yieldable(d)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
|
|
||||||
|
from netaddr import IPSet
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.defer import TimeoutError
|
from twisted.internet.defer import TimeoutError
|
||||||
from twisted.internet.error import ConnectingCancelledError, DNSLookupError
|
from twisted.internet.error import ConnectingCancelledError, DNSLookupError
|
||||||
@ -209,6 +211,75 @@ class FederationClientTests(HomeserverTestCase):
|
|||||||
self.assertIsInstance(f.value, RequestSendFailed)
|
self.assertIsInstance(f.value, RequestSendFailed)
|
||||||
self.assertIsInstance(f.value.inner_exception, ResponseNeverReceived)
|
self.assertIsInstance(f.value.inner_exception, ResponseNeverReceived)
|
||||||
|
|
||||||
|
def test_client_ip_range_blacklist(self):
|
||||||
|
"""Ensure that Synapse does not try to connect to blacklisted IPs"""
|
||||||
|
|
||||||
|
# Set up the ip_range blacklist
|
||||||
|
self.hs.config.federation_ip_range_blacklist = IPSet([
|
||||||
|
"127.0.0.0/8",
|
||||||
|
"fe80::/64",
|
||||||
|
])
|
||||||
|
self.reactor.lookups["internal"] = "127.0.0.1"
|
||||||
|
self.reactor.lookups["internalv6"] = "fe80:0:0:0:0:8a2e:370:7337"
|
||||||
|
self.reactor.lookups["fine"] = "10.20.30.40"
|
||||||
|
cl = MatrixFederationHttpClient(self.hs, None)
|
||||||
|
|
||||||
|
# Try making a GET request to a blacklisted IPv4 address
|
||||||
|
# ------------------------------------------------------
|
||||||
|
# Make the request
|
||||||
|
d = cl.get_json("internal:8008", "foo/bar", timeout=10000)
|
||||||
|
|
||||||
|
# Nothing happened yet
|
||||||
|
self.assertNoResult(d)
|
||||||
|
|
||||||
|
self.pump(1)
|
||||||
|
|
||||||
|
# Check that it was unable to resolve the address
|
||||||
|
clients = self.reactor.tcpClients
|
||||||
|
self.assertEqual(len(clients), 0)
|
||||||
|
|
||||||
|
f = self.failureResultOf(d)
|
||||||
|
self.assertIsInstance(f.value, RequestSendFailed)
|
||||||
|
self.assertIsInstance(f.value.inner_exception, DNSLookupError)
|
||||||
|
|
||||||
|
# Try making a POST request to a blacklisted IPv6 address
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# Make the request
|
||||||
|
d = cl.post_json("internalv6:8008", "foo/bar", timeout=10000)
|
||||||
|
|
||||||
|
# Nothing has happened yet
|
||||||
|
self.assertNoResult(d)
|
||||||
|
|
||||||
|
# Move the reactor forwards
|
||||||
|
self.pump(1)
|
||||||
|
|
||||||
|
# Check that it was unable to resolve the address
|
||||||
|
clients = self.reactor.tcpClients
|
||||||
|
self.assertEqual(len(clients), 0)
|
||||||
|
|
||||||
|
# Check that it was due to a blacklisted DNS lookup
|
||||||
|
f = self.failureResultOf(d, RequestSendFailed)
|
||||||
|
self.assertIsInstance(f.value.inner_exception, DNSLookupError)
|
||||||
|
|
||||||
|
# Try making a GET request to a non-blacklisted IPv4 address
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Make the request
|
||||||
|
d = cl.post_json("fine:8008", "foo/bar", timeout=10000)
|
||||||
|
|
||||||
|
# Nothing has happened yet
|
||||||
|
self.assertNoResult(d)
|
||||||
|
|
||||||
|
# Move the reactor forwards
|
||||||
|
self.pump(1)
|
||||||
|
|
||||||
|
# Check that it was able to resolve the address
|
||||||
|
clients = self.reactor.tcpClients
|
||||||
|
self.assertNotEqual(len(clients), 0)
|
||||||
|
|
||||||
|
# Connection will still fail as this IP address does not resolve to anything
|
||||||
|
f = self.failureResultOf(d, RequestSendFailed)
|
||||||
|
self.assertIsInstance(f.value.inner_exception, ConnectingCancelledError)
|
||||||
|
|
||||||
def test_client_gets_headers(self):
|
def test_client_gets_headers(self):
|
||||||
"""
|
"""
|
||||||
Once the client gets the headers, _request returns successfully.
|
Once the client gets the headers, _request returns successfully.
|
||||||
|
Loading…
Reference in New Issue
Block a user