Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add federation_domain_whitelist option #2820

Merged
merged 7 commits into from
Jan 22, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,32 @@ class RegistrationError(SynapseError):
pass


class FederationDeniedError(SynapseError):
"""An error raised when the server tries to federate with a server which
is not on its federation whitelist.

Attributes:
destination (str): The destination which has been denied
"""

def __init__(self, destination):
"""Raised by federation client or server to indicate that we are
are deliberately not attempting to contact a given server because it is
not on our federation whitelist.

Args:
destination (str): the domain in question
"""

self.destination = destination

super(FederationDeniedError, self).__init__(
code=403,
msg="Federation denied with %s." % (self.destination,),
errcode=Codes.FORBIDDEN,
)


class InteractiveAuthIncompleteError(Exception):
"""An error raised when UI auth is not yet complete

Expand Down
22 changes: 22 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ def read_config(self, config):
"block_non_admin_invites", False,
)

# FIXME: federation_domain_whitelist needs sytests
self.federation_domain_whitelist = None
federation_domain_whitelist = config.get(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels very odd that you can whitelist everything by having an empty whitelist. I wonder if it would be better to make an empty whitelist actually mean that nothing is allowed, and have the default be *

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point; fixed.

"federation_domain_whitelist", None
)
# turn the whitelist into a hash for speed of lookup
if federation_domain_whitelist is not None:
self.federation_domain_whitelist = {}
for domain in federation_domain_whitelist:
self.federation_domain_whitelist[domain] = True

if self.public_baseurl is not None:
if self.public_baseurl[-1] != '/':
self.public_baseurl += '/'
Expand Down Expand Up @@ -210,6 +221,17 @@ def default_config(self, server_name, **kwargs):
# (except those sent by local server admins). The default is False.
# block_non_admin_invites: True

# Restrict federation to the following whitelist of domains.
# N.B. we recommend also firewalling your federation listener to limit
# inbound federation traffic as early as possible, rather than relying
# purely on this application-layer restriction. If not specified, the
# default is to whitelist nothing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/nothing/everything?

#
# federation_domain_whitelist:
# - lon.example.com
# - nyc.example.com
# - syd.example.com

# List of ports that Synapse should listen on, their purpose and their
# configuration.
listeners:
Expand Down
5 changes: 4 additions & 1 deletion synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from synapse.api.constants import Membership
from synapse.api.errors import (
CodeMessageException, HttpResponseException, SynapseError,
CodeMessageException, HttpResponseException, SynapseError, FederationDeniedError
)
from synapse.events import builder
from synapse.federation.federation_base import (
Expand Down Expand Up @@ -266,6 +266,9 @@ def get_pdu(self, destinations, event_id, outlier=False, timeout=None):
except NotRetryingDestination as e:
logger.info(e.message)
continue
except FederationDeniedError as e:
logger.info(e.message)
continue
except Exception as e:
pdu_attempts[destination] = now

Expand Down
4 changes: 3 additions & 1 deletion synapse/federation/transaction_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .persistence import TransactionActions
from .units import Transaction, Edu

from synapse.api.errors import HttpResponseException
from synapse.api.errors import HttpResponseException, FederationDeniedError
from synapse.util import logcontext, PreserveLoggingContext
from synapse.util.async import run_on_reactor
from synapse.util.retryutils import NotRetryingDestination, get_retry_limiter
Expand Down Expand Up @@ -490,6 +490,8 @@ def _transaction_transmission_loop(self, destination):
(e.retry_last_ts + e.retry_interval) / 1000.0
),
)
except FederationDeniedError as e:
logger.info(e)
except Exception as e:
logger.warn(
"TX [%s] Failed to send transaction: %s",
Expand Down
3 changes: 3 additions & 0 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ def make_membership_event(self, destination, room_id, user_id, membership):

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if the remote destination
is not in our federation whitelist
"""
valid_memberships = {Membership.JOIN, Membership.LEAVE}
if membership not in valid_memberships:
Expand Down
9 changes: 8 additions & 1 deletion synapse/federation/transport/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from twisted.internet import defer

from synapse.api.urls import FEDERATION_PREFIX as PREFIX
from synapse.api.errors import Codes, SynapseError
from synapse.api.errors import Codes, SynapseError, FederationDeniedError
from synapse.http.server import JsonResource
from synapse.http.servlet import (
parse_json_object_from_request, parse_integer_from_args, parse_string_from_args,
Expand Down Expand Up @@ -81,6 +81,7 @@ def __init__(self, hs):
self.keyring = hs.get_keyring()
self.server_name = hs.hostname
self.store = hs.get_datastore()
self.federation_domain_whitelist = hs.config.federation_domain_whitelist

# A method just so we can pass 'self' as the authenticator to the Servlets
@defer.inlineCallbacks
Expand All @@ -92,6 +93,12 @@ def authenticate_request(self, request, content):
"signatures": {},
}

if (
self.federation_domain_whitelist is not None and
self.server_name not in self.federation_domain_whitelist
):
raise FederationDeniedError(self.server_name)

if content is not None:
json_request["content"] = content

Expand Down
4 changes: 4 additions & 0 deletions synapse/handlers/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.
from synapse.api import errors
from synapse.api.constants import EventTypes
from synapse.api.errors import FederationDeniedError
from synapse.util import stringutils
from synapse.util.async import Linearizer
from synapse.util.caches.expiringcache import ExpiringCache
Expand Down Expand Up @@ -513,6 +514,9 @@ def _handle_device_updates(self, user_id):
# This makes it more likely that the device lists will
# eventually become consistent.
return
except FederationDeniedError as e:
logger.info(e)
return
except Exception:
# TODO: Remember that we are now out of sync and try again
# later
Expand Down
8 changes: 7 additions & 1 deletion synapse/handlers/e2e_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from canonicaljson import encode_canonical_json
from twisted.internet import defer

from synapse.api.errors import SynapseError, CodeMessageException
from synapse.api.errors import (
SynapseError, CodeMessageException, FederationDeniedError,
)
from synapse.types import get_domain_from_id, UserID
from synapse.util.logcontext import preserve_fn, make_deferred_yieldable
from synapse.util.retryutils import NotRetryingDestination
Expand Down Expand Up @@ -140,6 +142,10 @@ def do_remote_query(destination):
failures[destination] = {
"status": 503, "message": "Not ready for retry",
}
except FederationDeniedError as e:
failures[destination] = {
"status": 403, "message": "Federation Denied",
}
except Exception as e:
# include ConnectionRefused and other errors
failures[destination] = {
Expand Down
4 changes: 4 additions & 0 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from synapse.api.errors import (
AuthError, FederationError, StoreError, CodeMessageException, SynapseError,
FederationDeniedError,
)
from synapse.api.constants import EventTypes, Membership, RejectedReason
from synapse.events.validator import EventValidator
Expand Down Expand Up @@ -782,6 +783,9 @@ def try_backfill(domains):
except NotRetryingDestination as e:
logger.info(e.message)
continue
except FederationDeniedError as e:
logger.info(e)
continue
except Exception as e:
logger.exception(
"Failed to backfill from %s because %s",
Expand Down
28 changes: 27 additions & 1 deletion synapse/http/matrixfederationclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from canonicaljson import encode_canonical_json

from synapse.api.errors import (
SynapseError, Codes, HttpResponseException,
SynapseError, Codes, HttpResponseException, FederationDeniedError,
)

from signedjson.sign import sign_json
Expand Down Expand Up @@ -123,11 +123,22 @@ def _request(self, destination, method, path,

Fails with ``HTTPRequestException``: if we get an HTTP response
code >= 300.

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist

(May also fail with plenty of other Exceptions for things like DNS
failures, connection failures, SSL failures.)
"""
if (
self.hs.config.federation_domain_whitelist and
destination not in self.hs.config.federation_domain_whitelist
):
raise FederationDeniedError(destination)

limiter = yield synapse.util.retryutils.get_retry_limiter(
destination,
self.clock,
Expand Down Expand Up @@ -308,6 +319,9 @@ def put_json(self, destination, path, data={}, json_data_callback=None,

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist
"""

if not json_data_callback:
Expand Down Expand Up @@ -368,6 +382,9 @@ def post_json(self, destination, path, data={}, long_retries=False,

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist
"""

def body_callback(method, url_bytes, headers_dict):
Expand Down Expand Up @@ -422,6 +439,9 @@ def get_json(self, destination, path, args={}, retry_on_dns_fail=True,

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist
"""
logger.debug("get_json args: %s", args)

Expand Down Expand Up @@ -475,6 +495,9 @@ def delete_json(self, destination, path, long_retries=False,

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist
"""

response = yield self._request(
Expand Down Expand Up @@ -518,6 +541,9 @@ def get_file(self, destination, path, output_stream, args={},

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist
"""

encoded_args = {}
Expand Down
8 changes: 8 additions & 0 deletions synapse/rest/key/v2/remote_key_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(self, hs):
self.store = hs.get_datastore()
self.version_string = hs.version_string
self.clock = hs.get_clock()
self.federation_domain_whitelist = hs.config.federation_domain_whitelist

def render_GET(self, request):
self.async_render_GET(request)
Expand Down Expand Up @@ -137,6 +138,13 @@ def query_keys(self, request, query, query_remote_on_cache_miss=False):
logger.info("Handling query for keys %r", query)
store_queries = []
for server_name, key_ids in query.items():
if (
self.federation_domain_whitelist is not None and
server_name not in self.federation_domain_whitelist
):
logger.debug("Federation denied with %s", server_name)
continue

if not key_ids:
key_ids = (None,)
for key_id in key_ids:
Expand Down
22 changes: 20 additions & 2 deletions synapse/rest/media/v1/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@

from synapse.http.matrixfederationclient import MatrixFederationHttpClient
from synapse.util.stringutils import random_string
from synapse.api.errors import SynapseError, HttpResponseException, \
NotFoundError
from synapse.api.errors import (
SynapseError, HttpResponseException, NotFoundError, FederationDeniedError,
)

from synapse.util.async import Linearizer
from synapse.util.stringutils import is_ascii
Expand Down Expand Up @@ -77,6 +78,8 @@ def __init__(self, hs):
self.recently_accessed_remotes = set()
self.recently_accessed_locals = set()

self.federation_domain_whitelist = hs.config.federation_domain_whitelist

# List of StorageProviders where we should search for media and
# potentially upload to.
storage_providers = []
Expand Down Expand Up @@ -222,6 +225,12 @@ def get_remote_media(self, request, server_name, media_id, name):
Deferred: Resolves once a response has successfully been written
to request
"""
if (
self.federation_domain_whitelist is not None and
server_name not in self.federation_domain_whitelist
):
raise FederationDeniedError(server_name)

self.mark_recently_accessed(server_name, media_id)

# We linearize here to ensure that we don't try and download remote
Expand Down Expand Up @@ -256,6 +265,12 @@ def get_remote_media_info(self, server_name, media_id):
Returns:
Deferred[dict]: The media_info of the file
"""
if (
self.federation_domain_whitelist is not None and
server_name not in self.federation_domain_whitelist
):
raise FederationDeniedError(server_name)

# We linearize here to ensure that we don't try and download remote
# media multiple times concurrently
key = (server_name, media_id)
Expand Down Expand Up @@ -371,6 +386,9 @@ def _download_remote_file(self, server_name, media_id, file_id):
except NotRetryingDestination:
logger.warn("Not retrying destination %r", server_name)
raise SynapseError(502, "Failed to fetch remote media")
except FederationDeniedError as e:
logger.info(e)
raise e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be raise rather than raise e, unless there is a good reason to throw a new exception annd hence swallow the original stack trace.

Although, looking at it, I don't think you'll ever get here, because FederationDeniedError is a subclass of SynapseError so the clause at line 382 will get taken instead. I think you might as well remove the whole thing

except Exception:
logger.exception("Failed to fetch remote media %s/%s",
server_name, media_id)
Expand Down
12 changes: 12 additions & 0 deletions synapse/util/retryutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@

class NotRetryingDestination(Exception):
def __init__(self, retry_last_ts, retry_interval, destination):
"""Raised by the limiter (and federation client) to indicate that we are
are deliberately not attempting to contact a given server.

Args:
retry_last_ts (int): the unix ts in milliseconds of our last attempt
to contact the server. 0 indicates that the last attempt was
successful or that we've never actually attempted to connect.
retry_interval (int): the time in milliseconds to wait until the next
attempt.
destination (str): the domain in question
"""

msg = "Not retrying server %s." % (destination,)
super(NotRetryingDestination, self).__init__(msg)

Expand Down
Loading