Example #1
0
class Server:
    """Server listening for incoming connections"""
    logger = logging.getLogger("p2p.server.Server")
    _server = None

    def __init__(
        self,
        privkey: datatypes.PrivateKey,
        server_address: Address,
        chaindb: AsyncChainDB,
        bootstrap_nodes: List[str],
        network_id: int,
        min_peers: int = DEFAULT_MIN_PEERS,
        peer_class: Type[BasePeer] = ETHPeer,
    ) -> None:
        self.cancel_token = CancelToken('Server')
        self.chaindb = chaindb
        self.privkey = privkey
        self.server_address = server_address
        self.network_id = network_id
        self.peer_class = peer_class
        # TODO: bootstrap_nodes should be looked up by network_id.
        discovery = DiscoveryProtocol(self.privkey,
                                      self.server_address,
                                      bootstrap_nodes=bootstrap_nodes)
        self.peer_pool = PeerPool(
            peer_class,
            self.chaindb,
            self.network_id,
            self.privkey,
            discovery,
            min_peers=min_peers,
        )

    async def start(self) -> None:
        self._server = await asyncio.start_server(
            self.receive_handshake,
            host=self.server_address.ip,
            port=self.server_address.tcp_port,
        )

    async def run(self) -> None:
        await self.start()
        self.logger.info("Running server...")
        self.logger.info(
            "enode://%s@%s:%s",
            self.privkey.public_key.to_hex()[2:],
            self.server_address.ip,
            self.server_address.tcp_port,
        )
        await self.cancel_token.wait()
        await self.stop()

    async def stop(self) -> None:
        self.logger.info("Closing server...")
        self.cancel_token.trigger()
        self._server.close()
        await self._server.wait_closed()
        await self.peer_pool.stop()

    async def receive_handshake(self, reader: asyncio.StreamReader,
                                writer: asyncio.StreamWriter) -> None:
        await wait_with_token(
            self._receive_handshake(reader, writer),
            token=self.cancel_token,
            timeout=HANDSHAKE_TIMEOUT,
        )

    async def _receive_handshake(self, reader: asyncio.StreamReader,
                                 writer: asyncio.StreamWriter) -> None:
        self.logger.debug("Receiving handshake...")
        # Use reader to read the auth_init msg until EOF
        msg = await wait_with_token(
            reader.read(ENCRYPTED_AUTH_MSG_LEN),
            token=self.cancel_token,
        )

        # Use decode_authentication(auth_init_message) on auth init msg
        try:
            ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication(
                msg, self.privkey)
        # Try to decode as EIP8
        except DecryptionError:
            msg_size = big_endian_to_int(msg[:2])
            remaining_bytes = msg_size - ENCRYPTED_AUTH_MSG_LEN + 2
            msg += await wait_with_token(
                reader.read(remaining_bytes),
                token=self.cancel_token,
            )
            ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication(
                msg, self.privkey)

        # Get remote's address: IPv4 or IPv6
        ip, socket, *_ = writer.get_extra_info("peername")
        remote_address = Address(ip, socket)

        # Create `HandshakeResponder(remote: kademlia.Node, privkey: datatypes.PrivateKey)` instance
        initiator_remote = Node(initiator_pubkey, remote_address)
        responder = HandshakeResponder(initiator_remote, self.privkey)

        # Call `HandshakeResponder.create_auth_ack_message(nonce: bytes)` to create the reply
        responder_nonce = secrets.token_bytes(HASH_LEN)
        auth_ack_msg = responder.create_auth_ack_message(nonce=responder_nonce)
        auth_ack_ciphertext = responder.encrypt_auth_ack_message(auth_ack_msg)

        # Use the `writer` to send the reply to the remote
        writer.write(auth_ack_ciphertext)
        await writer.drain()

        # Call `HandshakeResponder.derive_shared_secrets()` and use return values to create `Peer`
        aes_secret, mac_secret, egress_mac, ingress_mac = responder.derive_secrets(
            initiator_nonce=initiator_nonce,
            responder_nonce=responder_nonce,
            remote_ephemeral_pubkey=ephem_pubkey,
            auth_init_ciphertext=msg,
            auth_ack_ciphertext=auth_ack_ciphertext)

        # Create and register peer in peer_pool
        peer = self.peer_class(remote=initiator_remote,
                               privkey=self.privkey,
                               reader=reader,
                               writer=writer,
                               aes_secret=aes_secret,
                               mac_secret=mac_secret,
                               egress_mac=egress_mac,
                               ingress_mac=ingress_mac,
                               chaindb=self.chaindb,
                               network_id=self.network_id)

        await self.do_p2p_handshake(peer)

    async def do_p2p_handshake(self, peer: BasePeer) -> None:
        try:
            # P2P Handshake.
            await peer.do_p2p_handshake(),
        except (HandshakeFailure, asyncio.TimeoutError) as e:
            self.logger.debug('Unable to finish P2P handshake: %s', str(e))
        else:
            # Run peer and add peer.
            self.peer_pool.start_peer(peer)
Example #2
0
class Server(BaseService):
    """Server listening for incoming connections"""
    logger = logging.getLogger("p2p.server.Server")
    _server = None

    def __init__(
        self,
        privkey: datatypes.PrivateKey,
        server_address: Address,
        chaindb: AsyncChainDB,
        bootstrap_nodes: List[str],
        network_id: int,
        min_peers: int = DEFAULT_MIN_PEERS,
        peer_class: Type[BasePeer] = ETHPeer,
    ) -> None:
        super().__init__(CancelToken('Server'))
        self.chaindb = chaindb
        self.privkey = privkey
        self.server_address = server_address
        self.network_id = network_id
        self.peer_class = peer_class
        # TODO: bootstrap_nodes should be looked up by network_id.
        self.discovery = DiscoveryProtocol(self.privkey,
                                           self.server_address,
                                           bootstrap_nodes=bootstrap_nodes)
        self.peer_pool = PeerPool(
            peer_class,
            self.chaindb,
            self.network_id,
            self.privkey,
            self.discovery,
            min_peers=min_peers,
        )

    async def refresh_nat_portmap(self) -> None:
        """Run an infinite loop refreshing our NAT port mapping.

        On every iteration we configure the port mapping with a lifetime of 30 minutes and then
        sleep for that long as well.
        """
        lifetime = 30 * 60
        while not self.is_finished:
            self.logger.info("Setting up NAT portmap...")
            # This is experimental and it's OK if it fails, hence the bare except.
            try:
                await self._add_nat_portmap(lifetime)
            except Exception as e:
                if (isinstance(e, upnpclient.soap.SOAPError)
                        and e.args == (718, 'ConflictInMappingEntry')):
                    # An entry already exists with the parameters we specified. Maybe the router
                    # didn't clean it up after it expired or it has been configured by other piece
                    # of software, either way we should not override it.
                    # https://tools.ietf.org/id/draft-ietf-pcp-upnp-igd-interworking-07.html#errors
                    self.logger.info(
                        "NAT port mapping already configured, not overriding it"
                    )
                else:
                    self.logger.exception("Failed to setup NAT portmap")

            try:
                await wait_with_token(asyncio.sleep(lifetime),
                                      token=self.cancel_token)
            except OperationCancelled:
                break

    async def _add_nat_portmap(self, lifetime: int) -> None:
        loop = asyncio.get_event_loop()
        # Use loop.run_in_executor() because upnpclient.discover() is blocking and may take a
        # while to complete.
        devices = await wait_with_token(loop.run_in_executor(
            None, upnpclient.discover),
                                        token=self.cancel_token,
                                        timeout=2 * REPLY_TIMEOUT)
        if not devices:
            self.logger.info("No UPNP-enabled devices found")
            return
        device = devices[0]
        device.WANIPConn1.AddPortMapping(
            NewRemoteHost=device.WANIPConn1.GetExternalIPAddress()
            ['NewExternalIPAddress'],
            NewExternalPort=self.server_address.tcp_port,
            NewProtocol='TCP',
            NewInternalPort=self.server_address.tcp_port,
            NewInternalClient=self.server_address.ip,
            NewEnabled='1',
            NewPortMappingDescription='Created by Py-EVM',
            NewLeaseDuration=lifetime)
        self.logger.info("NAT port forwarding successfully setup")

    async def _start(self) -> None:
        self._server = await asyncio.start_server(
            self.receive_handshake,
            host=self.server_address.ip,
            port=self.server_address.tcp_port,
        )

    async def _close(self) -> None:
        self._server.close()
        await self._server.wait_closed()

    async def _run(self) -> None:
        self.logger.info("Running server...")
        await self._start()
        self.logger.info(
            "enode://%s@%s:%s",
            self.privkey.public_key.to_hex()[2:],
            self.server_address.ip,
            self.server_address.tcp_port,
        )
        await self.discovery.create_endpoint()
        asyncio.ensure_future(self.refresh_nat_portmap())
        asyncio.ensure_future(self.discovery.bootstrap())
        asyncio.ensure_future(self.peer_pool.run())
        await self.cancel_token.wait()

    async def _cleanup(self) -> None:
        self.logger.info("Closing server...")
        await self.peer_pool.cancel()
        await self.discovery.stop()
        await self._close()

    async def receive_handshake(self, reader: asyncio.StreamReader,
                                writer: asyncio.StreamWriter) -> None:
        expected_exceptions = (TimeoutError, PeerConnectionLost,
                               HandshakeFailure, asyncio.IncompleteReadError,
                               ConnectionResetError, BrokenPipeError)
        try:
            await self._receive_handshake(reader, writer)
        except expected_exceptions as e:
            self.logger.debug("Could not complete handshake", exc_info=True)
        except OperationCancelled:
            pass
        except Exception as e:
            self.logger.exception("Unexpected error handling handshake")

    async def _receive_handshake(self, reader: asyncio.StreamReader,
                                 writer: asyncio.StreamWriter) -> None:
        msg = await wait_with_token(
            reader.read(ENCRYPTED_AUTH_MSG_LEN),
            token=self.cancel_token,
            timeout=REPLY_TIMEOUT,
        )

        ip, socket, *_ = writer.get_extra_info("peername")
        remote_address = Address(ip, socket)
        self.logger.debug("Receiving handshake from %s", remote_address)
        try:
            ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication(
                msg, self.privkey)
        except DecryptionError:
            # Try to decode as EIP8
            msg_size = big_endian_to_int(msg[:2])
            remaining_bytes = msg_size - ENCRYPTED_AUTH_MSG_LEN + 2
            msg += await wait_with_token(
                reader.read(remaining_bytes),
                token=self.cancel_token,
                timeout=REPLY_TIMEOUT,
            )
            try:
                ephem_pubkey, initiator_nonce, initiator_pubkey = decode_authentication(
                    msg, self.privkey)
            except DecryptionError as e:
                self.logger.warn("Failed to decrypt handshake", exc_info=True)
                return

        # Create `HandshakeResponder(remote: kademlia.Node, privkey: datatypes.PrivateKey)` instance
        initiator_remote = Node(initiator_pubkey, remote_address)
        responder = HandshakeResponder(initiator_remote, self.privkey,
                                       self.cancel_token)

        # Call `HandshakeResponder.create_auth_ack_message(nonce: bytes)` to create the reply
        responder_nonce = secrets.token_bytes(HASH_LEN)
        auth_ack_msg = responder.create_auth_ack_message(nonce=responder_nonce)
        auth_ack_ciphertext = responder.encrypt_auth_ack_message(auth_ack_msg)

        # Use the `writer` to send the reply to the remote
        writer.write(auth_ack_ciphertext)
        await writer.drain()

        # Call `HandshakeResponder.derive_shared_secrets()` and use return values to create `Peer`
        aes_secret, mac_secret, egress_mac, ingress_mac = responder.derive_secrets(
            initiator_nonce=initiator_nonce,
            responder_nonce=responder_nonce,
            remote_ephemeral_pubkey=ephem_pubkey,
            auth_init_ciphertext=msg,
            auth_ack_ciphertext=auth_ack_ciphertext)

        # Create and register peer in peer_pool
        peer = self.peer_class(remote=initiator_remote,
                               privkey=self.privkey,
                               reader=reader,
                               writer=writer,
                               aes_secret=aes_secret,
                               mac_secret=mac_secret,
                               egress_mac=egress_mac,
                               ingress_mac=ingress_mac,
                               chaindb=self.chaindb,
                               network_id=self.network_id)

        await self.do_handshake(peer)

    async def do_handshake(self, peer: BasePeer) -> None:
        await peer.do_p2p_handshake(),
        await peer.do_sub_proto_handshake()
        self.peer_pool.start_peer(peer)