Skip to content

Commit

Permalink
Merge branch 'erikj/blacklist' into 'master'
Browse files Browse the repository at this point in the history
Add BlacklistingReactor

See merge request new-vector/sydent!6
  • Loading branch information
erikjohnston committed Apr 6, 2021
2 parents 29e7f4f + 3d531ed commit c951f17
Show file tree
Hide file tree
Showing 7 changed files with 613 additions and 19 deletions.
3 changes: 3 additions & 0 deletions matrix_is_test/launcher.py
Expand Up @@ -36,6 +36,9 @@
templates.path = {testsubject_path}/res
brand.default = is-test
ip.whitelist = 127.0.0.1
[email]
email.tlsmode = 0
email.invite.subject = %(sender_display_name)s has invited you to chat
Expand Down
155 changes: 155 additions & 0 deletions sydent/http/blacklisting_reactor.py
@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import (
Any,
List,
Optional,
)

from zope.interface import implementer, provider
from netaddr import IPAddress, IPSet

from twisted.internet.address import IPv4Address, IPv6Address
from twisted.internet.interfaces import (
IAddress,
IHostResolution,
IReactorPluggableNameResolver,
IResolutionReceiver,
)


logger = logging.getLogger(__name__)


def check_against_blacklist(
ip_address: IPAddress, ip_whitelist: Optional[IPSet], ip_blacklist: IPSet
) -> bool:
"""
Compares an IP address to allowed and disallowed IP sets.
Args:
ip_address: The IP address to check
ip_whitelist: Allowed IP addresses.
ip_blacklist: Disallowed IP addresses.
Returns:
True if the IP address is in the blacklist and not in the whitelist.
"""
if ip_address in ip_blacklist:
if ip_whitelist is None or ip_address not in ip_whitelist:
return True
return False


class _IPBlacklistingResolver:
"""
A proxy for reactor.nameResolver which only produces non-blacklisted IP
addresses, preventing DNS rebinding attacks on URL preview.
"""

def __init__(
self,
reactor: IReactorPluggableNameResolver,
ip_whitelist: Optional[IPSet],
ip_blacklist: IPSet,
):
"""
Args:
reactor: The twisted reactor.
ip_whitelist: IP addresses to allow.
ip_blacklist: IP addresses to disallow.
"""
self._reactor = reactor
self._ip_whitelist = ip_whitelist
self._ip_blacklist = ip_blacklist

def resolveHostName(
self, recv: IResolutionReceiver, hostname: str, portNumber: int = 0
) -> IResolutionReceiver:
addresses = [] # type: List[IAddress]

def _callback() -> None:
has_bad_ip = False
for address in addresses:
# We only expect IPv4 and IPv6 addresses since only A/AAAA lookups
# should go through this path.
if not isinstance(address, (IPv4Address, IPv6Address)):
continue

ip_address = IPAddress(address.host)

if check_against_blacklist(
ip_address, self._ip_whitelist, self._ip_blacklist
):
logger.info(
"Dropped %s from DNS resolution to %s due to blacklist"
% (ip_address, hostname)
)
has_bad_ip = True

# if we have a blacklisted IP, we'd like to raise an error to block the
# request, but all we can really do from here is claim that there were no
# valid results.
if not has_bad_ip:
for address in addresses:
recv.addressResolved(address)
recv.resolutionComplete()

@provider(IResolutionReceiver)
class EndpointReceiver:
@staticmethod
def resolutionBegan(resolutionInProgress: IHostResolution) -> None:
recv.resolutionBegan(resolutionInProgress)

@staticmethod
def addressResolved(address: IAddress) -> None:
addresses.append(address)

@staticmethod
def resolutionComplete() -> None:
_callback()

self._reactor.nameResolver.resolveHostName(
EndpointReceiver, hostname, portNumber=portNumber
)

return recv


@implementer(IReactorPluggableNameResolver)
class BlacklistingReactorWrapper:
"""
A Reactor wrapper which will prevent DNS resolution to blacklisted IP
addresses, to prevent DNS rebinding.
"""

def __init__(
self,
reactor: IReactorPluggableNameResolver,
ip_whitelist: Optional[IPSet],
ip_blacklist: IPSet,
):
self._reactor = reactor

# We need to use a DNS resolver which filters out blacklisted IP
# addresses, to prevent DNS rebinding.
self.nameResolver = _IPBlacklistingResolver(
self._reactor, ip_whitelist, ip_blacklist
)

def __getattr__(self, attr: str) -> Any:
# Passthrough to the real reactor except for the DNS resolver.
return getattr(self._reactor, attr)
17 changes: 13 additions & 4 deletions sydent/http/httpclient.py
Expand Up @@ -22,8 +22,9 @@
from twisted.internet import defer
from twisted.web.client import FileBodyProducer, Agent, readBody
from twisted.web.http_headers import Headers
from sydent.http.matrixfederationagent import MatrixFederationAgent

from sydent.http.blacklisting_reactor import BlacklistingReactorWrapper
from sydent.http.matrixfederationagent import MatrixFederationAgent
from sydent.http.federation_tls_options import ClientTLSOptionsFactory
from sydent.http.httpcommon import BodyExceededMaxSize, read_body_with_max_size

Expand Down Expand Up @@ -116,7 +117,11 @@ def __init__(self, sydent):
# BrowserLikePolicyForHTTPS context factory which will do regular cert validation
# 'like a browser'
self.agent = Agent(
self.sydent.reactor,
BlacklistingReactorWrapper(
reactor=self.sydent.reactor,
ip_whitelist=sydent.ip_whitelist,
ip_blacklist=sydent.ip_blacklist,
),
connectTimeout=15,
)

Expand All @@ -127,6 +132,10 @@ class FederationHttpClient(HTTPClient):
def __init__(self, sydent):
self.sydent = sydent
self.agent = MatrixFederationAgent(
self.sydent.reactor,
ClientTLSOptionsFactory(sydent.cfg),
BlacklistingReactorWrapper(
reactor=self.sydent.reactor,
ip_whitelist=sydent.ip_whitelist,
ip_blacklist=sydent.ip_blacklist,
),
ClientTLSOptionsFactory(sydent.cfg) if sydent.use_tls_for_federation else None,
)
40 changes: 39 additions & 1 deletion sydent/sydent.py
Expand Up @@ -24,6 +24,7 @@
import logging
import logging.handlers
import os
from typing import Set

import twisted.internet.reactor
from twisted.internet import task
Expand All @@ -46,6 +47,7 @@

from sydent.util.hash import sha256_and_url_safe_base64
from sydent.util.tokenutils import generateAlphanumericTokenOfLength
from sydent.util.ip_range import generate_ip_set, DEFAULT_IP_RANGE_BLACKLIST

from sydent.sign.ed25519 import SydentEd25519

Expand Down Expand Up @@ -107,6 +109,26 @@
# Whether clients and homeservers can register an association using v1 endpoints.
'enable_v1_associations': 'true',
'delete_tokens_on_bind': 'true',

# Prevent outgoing requests from being sent to the following blacklisted
# IP address CIDR ranges. If this option is not specified or empty then
# it defaults to private IP address ranges.
#
# The blacklist applies to all outbound requests except replication
# requests.
#
# (0.0.0.0 and :: are always blacklisted, whether or not they are
# explicitly listed here, since they correspond to unroutable
# addresses.)
'ip.blacklist': '',

# List of IP address CIDR ranges that should be allowed for outbound
# requests. This is useful for specifying exceptions to wide-ranging
# blacklisted target IP ranges.
#
# This whitelist overrides `ip.blacklist` and defaults to an empty
# list.
'ip.whitelist': '',
},
'db': {
'db.file': os.environ.get('SYDENT_DB_PATH', 'sydent.db'),
Expand Down Expand Up @@ -183,9 +205,10 @@


class Sydent:
def __init__(self, cfg, reactor=twisted.internet.reactor):
def __init__(self, cfg, reactor=twisted.internet.reactor, use_tls_for_federation=True):
self.reactor = reactor
self.config_file = get_config_file_path()
self.use_tls_for_federation = use_tls_for_federation

self.cfg = cfg

Expand Down Expand Up @@ -241,6 +264,15 @@ def __init__(self, cfg, reactor=twisted.internet.reactor):
self.cfg.get("general", "delete_tokens_on_bind")
)

ip_blacklist = set_from_comma_sep_string(self.cfg.get("general", "ip.blacklist"))
if not ip_blacklist:
ip_blacklist = DEFAULT_IP_RANGE_BLACKLIST

ip_whitelist = set_from_comma_sep_string(self.cfg.get("general", "ip.whitelist"))

self.ip_blacklist = generate_ip_set(ip_blacklist)
self.ip_whitelist = generate_ip_set(ip_whitelist)

self.default_web_client_location = self.cfg.get(
"email", "email.default_web_client_location"
)
Expand Down Expand Up @@ -512,6 +544,12 @@ def parse_cfg_bool(value):
return value.lower() == "true"


def set_from_comma_sep_string(rawstr: str) -> Set[str]:
if rawstr == '':
return set()
return {x.strip() for x in rawstr.split(',')}


def run_gc():
threshold = gc.get_threshold()
counts = gc.get_count()
Expand Down

0 comments on commit c951f17

Please sign in to comment.