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

Convert blacklisted IPv4 addresses to compatible IPv6 addresses. #9240

Merged
merged 9 commits into from
Feb 3, 2021
Merged
1 change: 1 addition & 0 deletions changelog.d/9240.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deny access to additional IP addresses by default.
6 changes: 6 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pid_file: DATADIR/homeserver.pid
# - '100.64.0.0/10'
# - '192.0.0.0/24'
# - '169.254.0.0/16'
# - '192.88.99.0/24'
# - '198.18.0.0/15'
# - '192.0.2.0/24'
# - '198.51.100.0/24'
Expand All @@ -177,6 +178,8 @@ pid_file: DATADIR/homeserver.pid
# - '::1/128'
# - 'fe80::/10'
# - 'fc00::/7'
# - '2001:db8::/32'
# - 'ff00::/8'
clokep marked this conversation as resolved.
Show resolved Hide resolved

# List of IP address CIDR ranges that should be allowed for federation,
# identity servers, push servers, and for checking key validity for
Expand Down Expand Up @@ -994,6 +997,7 @@ media_store_path: "DATADIR/media_store"
# - '100.64.0.0/10'
# - '192.0.0.0/24'
# - '169.254.0.0/16'
# - '192.88.99.0/24'
# - '198.18.0.0/15'
# - '192.0.2.0/24'
# - '198.51.100.0/24'
Expand All @@ -1002,6 +1006,8 @@ media_store_path: "DATADIR/media_store"
# - '::1/128'
# - 'fe80::/10'
# - 'fc00::/7'
# - '2001:db8::/32'
# - 'ff00::/8'
clokep marked this conversation as resolved.
Show resolved Hide resolved

# List of IP address CIDR ranges that the URL preview spider is allowed
# to access even if they are specified in url_preview_ip_range_blacklist.
Expand Down
19 changes: 9 additions & 10 deletions synapse/config/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
from collections import namedtuple
from typing import Dict, List

from netaddr import IPSet

from synapse.config.server import DEFAULT_IP_RANGE_BLACKLIST
from synapse.config.server import DEFAULT_IP_RANGE_BLACKLIST, generate_ip_set
from synapse.python_dependencies import DependencyException, check_requirements
from synapse.util.module_loader import load_module

Expand Down Expand Up @@ -187,16 +185,17 @@ def read_config(self, config, **kwargs):
"to work"
)

self.url_preview_ip_range_blacklist = IPSet(
config["url_preview_ip_range_blacklist"]
)

# we always blacklist '0.0.0.0' and '::', which are supposed to be
# unroutable addresses.
self.url_preview_ip_range_blacklist.update(["0.0.0.0", "::"])
self.url_preview_ip_range_blacklist = generate_ip_set(
config["url_preview_ip_range_blacklist"],
["0.0.0.0/8", "::"],
config_path=("url_preview_ip_range_blacklist",),
)

self.url_preview_ip_range_whitelist = IPSet(
config.get("url_preview_ip_range_whitelist", ())
self.url_preview_ip_range_whitelist = generate_ip_set(
config.get("url_preview_ip_range_whitelist", ()),
config_path=("url_preview_ip_range_blacklist",),
clokep marked this conversation as resolved.
Show resolved Hide resolved
)

self.url_preview_url_blacklist = config.get("url_preview_url_blacklist", ())
Expand Down
97 changes: 80 additions & 17 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import itertools
import logging
import os.path
import re
Expand All @@ -23,7 +24,7 @@

import attr
import yaml
from netaddr import IPSet
from netaddr import AddrFormatError, IPNetwork, IPSet

from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.util.stringutils import parse_and_validate_server_name
Expand All @@ -40,6 +41,66 @@
# in the list.
DEFAULT_BIND_ADDRESSES = ["::", "0.0.0.0"]


def _6to4(network: IPNetwork) -> IPNetwork:
"""Convert an IPv4 network into a 6to4 IPv6 network per RFC 3056."""

# 6to4 networks consist of:
# * 2002 as the first 16 bits
# * The first IPv4 address in the network hex-encoded as the next 32 bits
# * The new prefix length needs to include the bits from the 2002 prefix.
hex_network = hex(network.first)[2:]
hex_network = ("0" * (8 - len(hex_network))) + hex_network
return IPNetwork(
"2002:%s:%s::/%d" % (hex_network[:4], hex_network[4:], 16 + network.prefixlen,)
)


def generate_ip_set(
ip_addresses: Optional[Iterable[str]],
extra_addresses: Optional[Iterable[str]] = None,
config_path: Optional[Iterable[str]] = None,
) -> IPSet:
"""
Generate an IPSet from a list of IP addresses or CIDRs.

Additionally, for each IPv4 network in the list of IP addresses, also
includes the corresponding IPv6 networks.

This includes:

* IPv4-Compatible IPv6 Address (see RFC 4291, section 2.5.5.1)
* IPv4-Mapped IPv6 Address (see RFC 4291, section 2.5.5.1)
clokep marked this conversation as resolved.
Show resolved Hide resolved
* 6to4 Address (see RFC 3056, section 2)

Args:
ip_addresses: An iterable of IP addresses or CIDRs.
extra_addresses: An iterable of IP addresses or CIDRs.
config_path: The path in the configuration for error messages.

Returns:
A new IP set.
"""
result = IPSet()
for ip in itertools.chain(ip_addresses or (), extra_addresses or ()):
try:
network = IPNetwork(ip)
except AddrFormatError as e:
raise ConfigError(
"Invalid IP range provided: %s." % (ip,), config_path
) from e
result.add(network)

# It is possible that these already exist in the set, but that's OK.
if ":" not in str(network):
Copy link
Member

Choose a reason for hiding this comment

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

How is there not a network.is_ipv4()???

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think because every IPv4 network is technically an IPv6 network too since all of IPv4 is included in IPv6? 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So really all of this works around a bug in netaddr that it doesn't understand the different forms of IPv6 addresses really encompass the same networks as the IPv4 ones. 🤷

Copy link
Member

Choose a reason for hiding this comment

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

🤷

result.add(IPNetwork(network).ipv6(ipv4_compatible=True))
result.add(IPNetwork(network).ipv6(ipv4_compatible=False))
result.add(_6to4(network))

return result


# IP ranges that are considered private / unroutable / don't make sense.
DEFAULT_IP_RANGE_BLACKLIST = [
# Localhost
"127.0.0.0/8",
Expand All @@ -53,6 +114,8 @@
"192.0.0.0/24",
# Link-local networks.
"169.254.0.0/16",
# Formerly used for 6to4 relay.
"192.88.99.0/24",
# Testing networks.
"198.18.0.0/15",
"192.0.2.0/24",
Expand All @@ -66,6 +129,10 @@
"fe80::/10",
# Unique local addresses.
"fc00::/7",
# Testing networks.
"2001:db8::/32",
# Multicast.
"ff00::/8",
clokep marked this conversation as resolved.
Show resolved Hide resolved
]

DEFAULT_ROOM_VERSION = "6"
Expand Down Expand Up @@ -294,32 +361,28 @@ def read_config(self, config, **kwargs):
)

# Attempt to create an IPSet from the given ranges
try:
self.ip_range_blacklist = IPSet(ip_range_blacklist)
except Exception as e:
raise ConfigError("Invalid range(s) provided in ip_range_blacklist.") from e

# Always blacklist 0.0.0.0, ::
self.ip_range_blacklist.update(["0.0.0.0", "::"])
self.ip_range_blacklist = generate_ip_set(
ip_range_blacklist, ["0.0.0.0/8", "::"], config_path=("ip_range_blacklist",)
)

try:
self.ip_range_whitelist = IPSet(config.get("ip_range_whitelist", ()))
except Exception as e:
raise ConfigError("Invalid range(s) provided in ip_range_whitelist.") from e
self.ip_range_whitelist = generate_ip_set(
config.get("ip_range_whitelist", ()), config_path=("ip_range_whitelist",)
)

# The federation_ip_range_blacklist is used for backwards-compatibility
# and only applies to federation and identity servers. If it is not given,
# default to ip_range_blacklist.
federation_ip_range_blacklist = config.get(
"federation_ip_range_blacklist", ip_range_blacklist
)
try:
self.federation_ip_range_blacklist = IPSet(federation_ip_range_blacklist)
except Exception as e:
raise ConfigError(
"Invalid range(s) provided in federation_ip_range_blacklist."
) from e
# Always blacklist 0.0.0.0, ::
self.federation_ip_range_blacklist.update(["0.0.0.0", "::"])
self.federation_ip_range_blacklist = generate_ip_set(
federation_ip_range_blacklist,
["0.0.0.0/8", "::"],
config_path=("federation_ip_range_blacklist",),
)

self.start_pushers = config.get("start_pushers", True)

Expand Down
61 changes: 60 additions & 1 deletion tests/config/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

import yaml

from synapse.config.server import ServerConfig, is_threepid_reserved
from synapse.config._base import ConfigError
from synapse.config.server import ServerConfig, generate_ip_set, is_threepid_reserved

from tests import unittest

Expand Down Expand Up @@ -128,3 +129,61 @@ def test_listeners_set_correctly_open_private_ports_true(self):
)

self.assertEqual(conf["listeners"], expected_listeners)


class GenerateIpSetTestCase(unittest.TestCase):
def test_empty(self):
ip_set = generate_ip_set(())
self.assertFalse(ip_set)

ip_set = generate_ip_set((), ())
self.assertFalse(ip_set)

def test_generate(self):
"""Check adding IPv4 and IPv6 addresses."""
# IPv4 address
ip_set = generate_ip_set(("1.2.3.4",))
self.assertEqual(len(ip_set.iter_cidrs()), 4)

# IPv4 CIDR
ip_set = generate_ip_set(("1.2.3.4/24",))
self.assertEqual(len(ip_set.iter_cidrs()), 4)

# IPv6 address
ip_set = generate_ip_set(("2001:db8::8a2e:370:7334",))
self.assertEqual(len(ip_set.iter_cidrs()), 1)

# IPv6 CIDR
ip_set = generate_ip_set(("2001:db8::/104",))
self.assertEqual(len(ip_set.iter_cidrs()), 1)

# The addresses can overlap OK.
ip_set = generate_ip_set(("1.2.3.4", "::1.2.3.4"))
self.assertEqual(len(ip_set.iter_cidrs()), 4)

def test_extra(self):
"""Extra IP addresses are treated the same."""
ip_set = generate_ip_set((), ("1.2.3.4",))
self.assertEqual(len(ip_set.iter_cidrs()), 4)

ip_set = generate_ip_set(("1.1.1.1",), ("1.2.3.4",))
self.assertEqual(len(ip_set.iter_cidrs()), 8)

# They can duplicate without error.
ip_set = generate_ip_set(("1.2.3.4",), ("1.2.3.4",))
self.assertEqual(len(ip_set.iter_cidrs()), 4)

def test_bad_value(self):
"""An error should be raised if a bad value is passed in."""
with self.assertRaises(ConfigError):
generate_ip_set(("not-an-ip",))

with self.assertRaises(ConfigError):
generate_ip_set(("1.2.3.4/128",))

with self.assertRaises(ConfigError):
generate_ip_set((":::",))

# The following get treated as empty data.
self.assertFalse(generate_ip_set(None))
self.assertFalse(generate_ip_set({}))