Prevent reserved or invalid IP addresses from being used to run Ursula.

pull/2462/head
Kieran Prasch 2021-01-06 20:58:58 -08:00
parent 3e76547af1
commit 424e373a21
9 changed files with 89 additions and 30 deletions

View File

@ -104,6 +104,8 @@ class Character(Learner):
# #
# Operating Mode # Operating Mode
#
if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful' if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful'
# and whether maybe only Lawful characters have an interface # and whether maybe only Lawful characters have an interface
self.interface = self._interface_class(character=self) self.interface = self._interface_class(character=self)

View File

@ -17,7 +17,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import json import json
from collections import OrderedDict from collections import OrderedDict, defaultdict
import contextlib import contextlib
import maya import maya
@ -39,7 +39,6 @@ from constant_sorrow.constants import (
READY, READY,
INVALIDATED INVALIDATED
) )
from collections import OrderedDict, defaultdict
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import Encoding
@ -59,7 +58,6 @@ from typing import Dict, Iterable, List, Tuple, Union, Optional, Sequence, Set
from umbral import pre from umbral import pre
from umbral.keys import UmbralPublicKey from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature from umbral.signing import Signature
import nucypher import nucypher
@ -101,6 +99,7 @@ from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker from nucypher.network.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger from nucypher.utilities.logging import Logger
from nucypher.utilities.networking import validate_worker_ip
class Alice(Character, BlockchainPolicyAuthor): class Alice(Character, BlockchainPolicyAuthor):
@ -1258,9 +1257,13 @@ class Ursula(Teacher, Character, Worker):
if result > 0: if result > 0:
self.log.debug(f"Pruned {result} treasure maps.") self.log.debug(f"Pruned {result} treasure maps.")
def __preflight(self) -> None:
"""Called immediately before running services"""
validate_worker_ip(worker_ip=self.rest_interface.host)
def run(self, def run(self,
emitter: StdoutEmitter = None, emitter: StdoutEmitter = None,
discovery: bool = True, discovery: bool = True, # TODO: see below
availability: bool = True, availability: bool = True,
worker: bool = True, worker: bool = True,
pruning: bool = True, pruning: bool = True,
@ -1272,6 +1275,8 @@ class Ursula(Teacher, Character, Worker):
"""Schedule and start select ursula services, then optionally start the reactor.""" """Schedule and start select ursula services, then optionally start the reactor."""
self.__preflight()
# #
# Async loops ordered by schedule priority # Async loops ordered by schedule priority
# #

View File

@ -37,10 +37,11 @@ from nucypher.cli.literature import (
COLLECT_URSULA_IPV4_ADDRESS, COLLECT_URSULA_IPV4_ADDRESS,
CONFIRM_URSULA_IPV4_ADDRESS CONFIRM_URSULA_IPV4_ADDRESS
) )
from nucypher.cli.types import IPV4_ADDRESS from nucypher.cli.types import IPV4_ADDRESS, WORKER_IP
from nucypher.config.characters import StakeHolderConfiguration from nucypher.config.characters import StakeHolderConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_WORKER_IP_ADDRESS from nucypher.config.constants import NUCYPHER_ENVVAR_WORKER_IP_ADDRESS
from nucypher.config.node import CharacterConfiguration from nucypher.config.node import CharacterConfiguration
from nucypher.utilities.networking import InvalidWorkerIP, validate_worker_ip
from nucypher.utilities.networking import determine_external_ip_address, UnknownIPAddress from nucypher.utilities.networking import determine_external_ip_address, UnknownIPAddress
@ -119,12 +120,12 @@ def handle_invalid_configuration_file(emitter: StdoutEmitter,
raise # crash :-( raise # crash :-(
def collect_external_ip_address(emitter: StdoutEmitter, network: str, force: bool = False) -> str: def collect_worker_ip_address(emitter: StdoutEmitter, network: str, force: bool = False) -> str:
# From environment variable # TODO: remove this environment variable? # From environment variable # TODO: remove this environment variable?
ip = os.environ.get(NUCYPHER_ENVVAR_WORKER_IP_ADDRESS) ip = os.environ.get(NUCYPHER_ENVVAR_WORKER_IP_ADDRESS)
if ip: if ip:
message = f'Using IP address from {NUCYPHER_ENVVAR_WORKER_IP_ADDRESS} environment variable' message = f'Using IP address ({ip}) from {NUCYPHER_ENVVAR_WORKER_IP_ADDRESS} environment variable'
emitter.message(message, verbosity=2) emitter.message(message, verbosity=2)
return ip return ip
@ -141,8 +142,9 @@ def collect_external_ip_address(emitter: StdoutEmitter, network: str, force: boo
# Confirmation # Confirmation
if not force: if not force:
if not click.confirm(CONFIRM_URSULA_IPV4_ADDRESS.format(rest_host=ip)): if not click.confirm(CONFIRM_URSULA_IPV4_ADDRESS.format(rest_host=ip)):
ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=IPV4_ADDRESS) ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=WORKER_IP)
validate_worker_ip(worker_ip=ip)
return ip return ip
@ -157,7 +159,17 @@ def perform_ip_checkup(emitter: StdoutEmitter, ursula: Ursula, force: bool = Fal
message = 'Cannot automatically determine external IP address' message = 'Cannot automatically determine external IP address'
emitter.message(message) emitter.message(message)
return # TODO: crash, or not to crash... that is the question return # TODO: crash, or not to crash... that is the question
ip_mismatch = external_ip != ursula.rest_interface.host rest_host = ursula.rest_interface.host
try:
validate_worker_ip(worker_ip=rest_host)
except InvalidWorkerIP:
message = f'{rest_host} is not a valid or permitted worker IP address. Set the correct external IP then try again\n' \
f'automatic configuration -> nucypher ursula config ip-address\n' \
f'manual configuration -> nucypher ursula config --rest-host <IP ADDRESS>'
emitter.message(message)
return
ip_mismatch = external_ip != rest_host
if ip_mismatch and not force: if ip_mismatch and not force:
error = f'\nX External IP address ({external_ip}) does not match configuration ({ursula.rest_interface.host}).\n' error = f'\nX External IP address ({external_ip}) does not match configuration ({ursula.rest_interface.host}).\n'
hint = f"Run 'nucypher ursula config ip-address' to reconfigure the IP address then try " \ hint = f"Run 'nucypher ursula config ip-address' to reconfigure the IP address then try " \

View File

@ -18,12 +18,14 @@
import click import click
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.signers.software import ClefSigner from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import ( from nucypher.cli.actions.configure import (
destroy_configuration, destroy_configuration,
handle_missing_configuration_file, handle_missing_configuration_file,
get_or_update_configuration, collect_external_ip_address get_or_update_configuration,
collect_worker_ip_address
) )
from nucypher.cli.actions.configure import forget as forget_nodes, perform_ip_checkup from nucypher.cli.actions.configure import forget as forget_nodes, perform_ip_checkup
from nucypher.cli.actions.select import ( from nucypher.cli.actions.select import (
@ -59,7 +61,7 @@ from nucypher.cli.options import (
option_max_gas_price option_max_gas_price
) )
from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.painting.help import paint_new_installation_help
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT, WORKER_IP
from nucypher.cli.utils import make_cli_character, setup_emitter from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import UrsulaConfiguration from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import ( from nucypher.config.constants import (
@ -176,7 +178,7 @@ class UrsulaConfigOptions:
# Resolve rest host # Resolve rest host
if not self.rest_host: if not self.rest_host:
self.rest_host = collect_external_ip_address(emitter, network=self.domain, force=force) self.rest_host = collect_worker_ip_address(emitter, network=self.domain, force=force)
return UrsulaConfiguration.generate(password=get_nucypher_password(confirm=True), return UrsulaConfiguration.generate(password=get_nucypher_password(confirm=True),
config_root=config_root, config_root=config_root,
@ -223,10 +225,10 @@ group_config_options = group_options(
max_gas_price=option_max_gas_price, max_gas_price=option_max_gas_price,
worker_address=click.option('--worker-address', help="Run the worker-ursula with a specified address", type=EIP55_CHECKSUM_ADDRESS), worker_address=click.option('--worker-address', help="Run the worker-ursula with a specified address", type=EIP55_CHECKSUM_ADDRESS),
federated_only=option_federated_only, federated_only=option_federated_only,
rest_host=click.option('--rest-host', help="The host IP address to run Ursula network services on", type=click.STRING), rest_host=click.option('--rest-host', help="The host IP address to run Ursula network services on", type=WORKER_IP),
rest_port=click.option('--rest-port', help="The host port to run Ursula network services on", type=NETWORK_PORT), rest_port=click.option('--rest-port', help="The host port to run Ursula network services on", type=NETWORK_PORT),
db_filepath=option_db_filepath, db_filepath=option_db_filepath,
network=option_network(), network=option_network(default=NetworksInventory.DEFAULT),
registry_filepath=option_registry_filepath, registry_filepath=option_registry_filepath,
poa=option_poa, poa=option_poa,
light=option_light, light=option_light,
@ -422,7 +424,7 @@ def config(general_config, config_options, config_file, force, action):
checksum_address=config_options.worker_address, checksum_address=config_options.worker_address,
config_class=UrsulaConfiguration) config_class=UrsulaConfiguration)
if action == 'ip-address': if action == 'ip-address':
rest_host = collect_external_ip_address(emitter=emitter, network=config_options.domain, force=force) rest_host = collect_worker_ip_address(emitter=emitter, network=config_options.domain, force=force)
config_options.rest_host = rest_host config_options.rest_host = rest_host
updates = config_options.get_updates() updates = config_options.get_updates()
get_or_update_configuration(emitter=emitter, get_or_update_configuration(emitter=emitter,

View File

@ -377,7 +377,7 @@ DECRYPTING_CHARACTER_KEYRING = 'Authenticating {name}'
CONFIRM_URSULA_IPV4_ADDRESS = "Detected IPv4 address ({rest_host}) - Is this the public-facing address of Ursula?" CONFIRM_URSULA_IPV4_ADDRESS = "Detected IPv4 address ({rest_host}) - Is this the public-facing address of Ursula?"
COLLECT_URSULA_IPV4_ADDRESS = "Enter Ursula's public-facing IPv4 address:" COLLECT_URSULA_IPV4_ADDRESS = "Enter Ursula's public-facing IPv4 address"
# #

View File

@ -26,6 +26,7 @@ from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.networks import NetworksInventory from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.token import NU from nucypher.blockchain.eth.token import NU
from nucypher.utilities.networking import validate_worker_ip, InvalidWorkerIP
class ChecksumAddress(click.ParamType): class ChecksumAddress(click.ParamType):
@ -46,12 +47,24 @@ class IPv4Address(click.ParamType):
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
try: try:
_address = ip_address(value) _address = ip_address(value)
except ValueError as e: except ValueError:
self.fail("Invalid IP Address") self.fail("Invalid IP Address")
else: else:
return value return value
class WorkerIPAddress(IPv4Address):
name = 'worker_ip'
def convert(self, value, param, ctx):
_ip = super().convert(value, param, ctx)
try:
validate_worker_ip(worker_ip=_ip)
except InvalidWorkerIP as e:
self.fail(str(e))
return value
class DecimalType(click.ParamType): class DecimalType(click.ParamType):
name = 'decimal' name = 'decimal'
@ -136,6 +149,7 @@ EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True,
# Network # Network
NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False) NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address() IPV4_ADDRESS = IPv4Address()
WORKER_IP = WorkerIPAddress()
GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys())) GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys()))
UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex() UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex()

View File

@ -39,7 +39,6 @@ class UrsulaConfiguration(CharacterConfiguration):
CHARACTER_CLASS = Ursula CHARACTER_CLASS = Ursula
NAME = CHARACTER_CLASS.__name__.lower() NAME = CHARACTER_CLASS.__name__.lower()
DEFAULT_REST_HOST = '127.0.0.1'
DEFAULT_REST_PORT = 9151 DEFAULT_REST_PORT = 9151
DEFAULT_DEVELOPMENT_REST_PORT = 10151 DEFAULT_DEVELOPMENT_REST_PORT = 10151
__DEFAULT_TLS_CURVE = ec.SECP384R1 __DEFAULT_TLS_CURVE = ec.SECP384R1
@ -48,10 +47,10 @@ class UrsulaConfiguration(CharacterConfiguration):
LOCAL_SIGNERS_ALLOWED = True LOCAL_SIGNERS_ALLOWED = True
def __init__(self, def __init__(self,
rest_host: str,
worker_address: str = None, worker_address: str = None,
dev_mode: bool = False, dev_mode: bool = False,
db_filepath: str = None, db_filepath: str = None,
rest_host: str = None,
rest_port: int = None, rest_port: int = None,
tls_curve: EllipticCurve = None, tls_curve: EllipticCurve = None,
certificate: Certificate = None, certificate: Certificate = None,
@ -64,7 +63,7 @@ class UrsulaConfiguration(CharacterConfiguration):
else: else:
rest_port = self.DEFAULT_REST_PORT rest_port = self.DEFAULT_REST_PORT
self.rest_port = rest_port self.rest_port = rest_port
self.rest_host = rest_host or self.DEFAULT_REST_HOST self.rest_host = rest_host
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.certificate = certificate self.certificate = certificate
self.db_filepath = db_filepath or UNINITIALIZED_CONFIGURATION self.db_filepath = db_filepath or UNINITIALIZED_CONFIGURATION

View File

@ -245,8 +245,7 @@ class Learner:
from nucypher.characters.lawful import Ursula from nucypher.characters.lawful import Ursula
self.node_class = node_class or Ursula self.node_class = node_class or Ursula
self.node_class.set_cert_storage_function( self.node_class.set_cert_storage_function(node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
known_nodes = known_nodes or tuple() known_nodes = known_nodes or tuple()
self.unresponsive_startup_nodes = list() # TODO: Buckets - Attempt to use these again later #567 self.unresponsive_startup_nodes = list() # TODO: Buckets - Attempt to use these again later #567

View File

@ -16,16 +16,15 @@
""" """
from ipaddress import ip_address
import random import random
import requests import requests
from ipaddress import ip_address
from requests.exceptions import RequestException, HTTPError from requests.exceptions import RequestException, HTTPError
from typing import Union from typing import Union
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry from nucypher.config.storages import LocalFileBasedNodeStorage
from nucypher.acumen.perception import FleetSensor from nucypher.acumen.perception import FleetSensor
from nucypher.characters.lawful import Ursula from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.network.middleware import RestMiddleware, NucypherMiddlewareClient from nucypher.network.middleware import RestMiddleware, NucypherMiddlewareClient
from nucypher.utilities.logging import Logger from nucypher.utilities.logging import Logger
@ -34,6 +33,10 @@ class UnknownIPAddress(RuntimeError):
pass pass
class InvalidWorkerIP(RuntimeError):
"""Raised when an Ursula is using an invalid IP address for it's server."""
RequestErrors = ( RequestErrors = (
# https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions # https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions
ConnectionError, ConnectionError,
@ -42,9 +45,22 @@ RequestErrors = (
HTTPError HTTPError
) )
RESERVED_IP_ADDRESSES = (
'0.0.0.0',
'127.0.0.1',
'1.2.3.4'
)
IP_DETECTION_LOGGER = Logger('external-ip-detection') IP_DETECTION_LOGGER = Logger('external-ip-detection')
def validate_worker_ip(worker_ip: str) -> None:
if worker_ip in RESERVED_IP_ADDRESSES:
raise InvalidWorkerIP(f'{worker_ip} is not a valid or permitted worker IP address. '
f'Verify the rest_host is set to the external IPV4 address')
def __request(url: str, certificate=None) -> Union[str, None]: def __request(url: str, certificate=None) -> Union[str, None]:
""" """
Utility function to send a GET request to a URL returning it's Utility function to send a GET request to a URL returning it's
@ -65,6 +81,8 @@ def get_external_ip_from_default_teacher(network: str,
log: Logger = IP_DETECTION_LOGGER, log: Logger = IP_DETECTION_LOGGER,
registry: BaseContractRegistry = None registry: BaseContractRegistry = None
) -> Union[str, None]: ) -> Union[str, None]:
from nucypher.characters.lawful import Ursula
if federated_only and registry: if federated_only and registry:
raise ValueError('Federated mode must not be true if registry is provided.') raise ValueError('Federated mode must not be true if registry is provided.')
base_error = 'Cannot determine IP using default teacher' base_error = 'Cannot determine IP using default teacher'
@ -76,13 +94,19 @@ def get_external_ip_from_default_teacher(network: str,
except KeyError: except KeyError:
log.debug(f'{base_error}: Unknown network "{network}".') log.debug(f'{base_error}: Unknown network "{network}".')
return return
if not registry:
# Registry is needed to perform on-chain staking verification. ####
registry = InMemoryContractRegistry.from_latest_publication(network=network) # TODO: Clean this mess #1481
node_storage = LocalFileBasedNodeStorage(federated_only=federated_only)
Ursula.set_cert_storage_function(node_storage.store_node_certificate)
Ursula.set_federated_mode(federated_only)
#####
teacher = Ursula.from_teacher_uri(teacher_uri=top_teacher_url, teacher = Ursula.from_teacher_uri(teacher_uri=top_teacher_url,
registry=registry,
federated_only=federated_only, federated_only=federated_only,
min_stake=0) # TODO: Handle customized min stake here. min_stake=0) # TODO: Handle customized min stake here.
# TODO: Pass registry here to verify stake (not essential here since it's a hardcoded node)
client = NucypherMiddlewareClient() client = NucypherMiddlewareClient()
try: try:
response = client.get(node_or_sprout=teacher, path=f"ping", timeout=2) # TLS certificate logic within response = client.get(node_or_sprout=teacher, path=f"ping", timeout=2) # TLS certificate logic within
@ -97,6 +121,8 @@ def get_external_ip_from_default_teacher(network: str,
raise UnknownIPAddress(error) raise UnknownIPAddress(error)
log.info(f'Fetched external IP address from default teacher ({top_teacher_url} reported {ip}).') log.info(f'Fetched external IP address from default teacher ({top_teacher_url} reported {ip}).')
return ip return ip
else:
log.debug(f'Failed to get external IP from teacher node ({response.status_code})')
def get_external_ip_from_known_nodes(known_nodes: FleetSensor, def get_external_ip_from_known_nodes(known_nodes: FleetSensor,