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
#
if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful'
# and whether maybe only Lawful characters have an interface
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
from collections import OrderedDict
from collections import OrderedDict, defaultdict
import contextlib
import maya
@ -39,7 +39,6 @@ from constant_sorrow.constants import (
READY,
INVALIDATED
)
from collections import OrderedDict, defaultdict
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
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.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from umbral.pre import UmbralCorrectnessError
from umbral.signing import Signature
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.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger
from nucypher.utilities.networking import validate_worker_ip
class Alice(Character, BlockchainPolicyAuthor):
@ -1258,9 +1257,13 @@ class Ursula(Teacher, Character, Worker):
if result > 0:
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,
emitter: StdoutEmitter = None,
discovery: bool = True,
discovery: bool = True, # TODO: see below
availability: bool = True,
worker: 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."""
self.__preflight()
#
# Async loops ordered by schedule priority
#

View File

@ -37,10 +37,11 @@ from nucypher.cli.literature import (
COLLECT_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.constants import NUCYPHER_ENVVAR_WORKER_IP_ADDRESS
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
@ -119,12 +120,12 @@ def handle_invalid_configuration_file(emitter: StdoutEmitter,
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?
ip = os.environ.get(NUCYPHER_ENVVAR_WORKER_IP_ADDRESS)
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)
return ip
@ -141,8 +142,9 @@ def collect_external_ip_address(emitter: StdoutEmitter, network: str, force: boo
# Confirmation
if not force:
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
@ -157,7 +159,17 @@ def perform_ip_checkup(emitter: StdoutEmitter, ursula: Ursula, force: bool = Fal
message = 'Cannot automatically determine external IP address'
emitter.message(message)
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:
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 " \

View File

@ -18,12 +18,14 @@
import click
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
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.select import (
@ -59,7 +61,7 @@ from nucypher.cli.options import (
option_max_gas_price
)
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.config.characters import UrsulaConfiguration
from nucypher.config.constants import (
@ -176,7 +178,7 @@ class UrsulaConfigOptions:
# Resolve 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),
config_root=config_root,
@ -223,10 +225,10 @@ group_config_options = group_options(
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),
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),
db_filepath=option_db_filepath,
network=option_network(),
network=option_network(default=NetworksInventory.DEFAULT),
registry_filepath=option_registry_filepath,
poa=option_poa,
light=option_light,
@ -422,7 +424,7 @@ def config(general_config, config_options, config_file, force, action):
checksum_address=config_options.worker_address,
config_class=UrsulaConfiguration)
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
updates = config_options.get_updates()
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?"
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.networks import NetworksInventory
from nucypher.blockchain.eth.token import NU
from nucypher.utilities.networking import validate_worker_ip, InvalidWorkerIP
class ChecksumAddress(click.ParamType):
@ -46,12 +47,24 @@ class IPv4Address(click.ParamType):
def convert(self, value, param, ctx):
try:
_address = ip_address(value)
except ValueError as e:
except ValueError:
self.fail("Invalid IP Address")
else:
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):
name = 'decimal'
@ -136,6 +149,7 @@ EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True,
# Network
NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address()
WORKER_IP = WorkerIPAddress()
GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys()))
UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex()

View File

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

View File

@ -245,8 +245,7 @@ class Learner:
from nucypher.characters.lawful import Ursula
self.node_class = node_class or Ursula
self.node_class.set_cert_storage_function(
node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
self.node_class.set_cert_storage_function(node_storage.store_node_certificate) # TODO: Fix this temporary workaround for on-disk cert storage. #1481
known_nodes = known_nodes or tuple()
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 requests
from ipaddress import ip_address
from requests.exceptions import RequestException, HTTPError
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.characters.lawful import Ursula
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.network.middleware import RestMiddleware, NucypherMiddlewareClient
from nucypher.utilities.logging import Logger
@ -34,6 +33,10 @@ class UnknownIPAddress(RuntimeError):
pass
class InvalidWorkerIP(RuntimeError):
"""Raised when an Ursula is using an invalid IP address for it's server."""
RequestErrors = (
# https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions
ConnectionError,
@ -42,9 +45,22 @@ RequestErrors = (
HTTPError
)
RESERVED_IP_ADDRESSES = (
'0.0.0.0',
'127.0.0.1',
'1.2.3.4'
)
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]:
"""
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,
registry: BaseContractRegistry = None
) -> Union[str, None]:
from nucypher.characters.lawful import Ursula
if federated_only and registry:
raise ValueError('Federated mode must not be true if registry is provided.')
base_error = 'Cannot determine IP using default teacher'
@ -76,13 +94,19 @@ def get_external_ip_from_default_teacher(network: str,
except KeyError:
log.debug(f'{base_error}: Unknown network "{network}".')
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,
registry=registry,
federated_only=federated_only,
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()
try:
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)
log.info(f'Fetched external IP address from default teacher ({top_teacher_url} reported {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,