mirror of https://github.com/nucypher/nucypher.git
Prevent reserved or invalid IP addresses from being used to run Ursula.
parent
3e76547af1
commit
424e373a21
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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 " \
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue