Example #1
0
    def __init__(self,
                 conf_dir,
                 addr,
                 api_addr,
                 run_local_api=False,
                 port=None,
                 prom_export=None):
        """
        Initialize an instance of the class SCIONDaemon.
        """
        super().__init__("sciond",
                         conf_dir,
                         prom_export=prom_export,
                         public=[(addr, port)])
        up_labels = {**self._labels, "type": "up"} if self._labels else None
        down_labels = {
            **self._labels, "type": "down"
        } if self._labels else None
        core_labels = {
            **self._labels, "type": "core"
        } if self._labels else None
        self.up_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL,
                                         labels=up_labels)
        self.down_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL,
                                           labels=down_labels)
        self.core_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL,
                                           labels=core_labels)
        self.peer_revs = RevCache()
        # Keep track of requested paths.
        self.requested_paths = ExpiringDict(self.MAX_REQS, self.PATH_REQ_TOUT)
        self.req_path_lock = threading.Lock()
        self._api_sock = None
        self.daemon_thread = None
        os.makedirs(SCIOND_API_SOCKDIR, exist_ok=True)
        self.api_addr = (api_addr or os.path.join(
            SCIOND_API_SOCKDIR, "%s.sock" % self.addr.isd_as))

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.REPLY: self.handle_path_reply,
                PMT.REVOCATION: self.handle_revocation,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }

        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self.handle_scmp_revocation
            },
        }

        if run_local_api:
            self._api_sock = ReliableSocket(bind_unix=(self.api_addr,
                                                       "sciond"))
            self._socks.add(self._api_sock, self.handle_accept)
Example #2
0
    def __init__(self, server_id, conf_dir, prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param str prom_export: prometheus export address.
        """
        super().__init__(server_id, conf_dir, prom_export=prom_export)
        # TODO: add 2 policies
        self.path_policy = PathPolicy.from_file(
            os.path.join(conf_dir, PATH_POLICY_FILE))
        self.signing_key = get_sig_key(self.conf_dir)
        self.of_gen_key = kdf(self.config.master_as_key, b"Derive OF Key")
        self.hashtree_gen_key = kdf(self.config.master_as_key,
                                    b"Derive hashtree Key")
        logging.info(self.config.__dict__)
        self._hash_tree = None
        self._hash_tree_lock = Lock()
        self._next_tree = None
        self._init_hash_tree()
        self.ifid_state = {}
        for ifid in self.ifid2br:
            self.ifid_state[ifid] = InterfaceState()
        self.ifid_state_lock = RLock()
        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PCB: {
                None: self.handle_pcb
            },
            PayloadClass.IFID: {
                None: self.handle_ifid_packet
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
            PayloadClass.PATH: {
                PMT.IFSTATE_REQ: self._handle_ifstate_request,
                PMT.REVOCATION: self._handle_revocation,
            },
        }
        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.addr.isd_as, BEACON_SERVICE, zkid,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.pcb_cache = ZkSharedCache(self.zk, self.ZK_PCB_CACHE_PATH,
                                       self._handle_pcbs_from_zk)
        self.revobjs_cache = ZkSharedCache(self.zk, self.ZK_REVOCATIONS_PATH,
                                           self.process_rev_objects)
        self.local_rev_cache = ExpiringDict(
            1000, HASHTREE_EPOCH_TIME + HASHTREE_EPOCH_TOLERANCE)
        self._rev_seg_lock = RLock()
Example #3
0
 def __init__(self, server_id, conf_dir, prom_export=None):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     :param str prom_export: prometheus export address.
     """
     super().__init__(server_id, conf_dir, prom_export=prom_export)
     down_labels = {
         **self._labels, "type": "down"
     } if self._labels else None
     core_labels = {
         **self._labels, "type": "core"
     } if self._labels else None
     self.down_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO,
                                        labels=down_labels)
     self.core_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO,
                                        labels=core_labels)
     self.pending_req = defaultdict(list)  # Dict of pending requests.
     self.pen_req_lock = threading.Lock()
     self._request_logger = None
     # Used when l/cPS doesn't have up/dw-path.
     self.waiting_targets = defaultdict(list)
     self.revocations = RevCache(labels=self._labels)
     # A mapping from (hash tree root of AS, IFID) to segments
     self.htroot_if2seg = ExpiringDict(1000, HASHTREE_TTL)
     self.htroot_if2seglock = Lock()
     self.CTRL_PLD_CLASS_MAP = {
         PayloadClass.PATH: {
             PMT.REQUEST: self.path_resolution,
             PMT.REPLY: self.handle_path_segment_record,
             PMT.REG: self.handle_path_segment_record,
             PMT.REVOCATION: self._handle_revocation,
             PMT.SYNC: self.handle_path_segment_record,
         },
         PayloadClass.CERT: {
             CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
             CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
             CertMgmtType.TRC_REPLY: self.process_trc_reply,
             CertMgmtType.TRC_REQ: self.process_trc_request,
         },
     }
     self.SCMP_PLD_CLASS_MAP = {
         SCMPClass.PATH: {
             SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
         },
     }
     self._segs_to_zk = ExpiringDict(1000, self.SEGS_TO_ZK_TTL)
     self._revs_to_zk = ExpiringDict(1000, HASHTREE_EPOCH_TIME)
     self._zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                   [(self.addr.host, self._port)])
     self.zk = Zookeeper(self.topology.isd_as, PATH_SERVICE,
                         self._zkid.copy().pack(), self.topology.zookeepers)
     self.zk.retry("Joining party", self.zk.party_setup)
     self.path_cache = ZkSharedCache(self.zk, self.ZK_PATH_CACHE_PATH,
                                     self._handle_paths_from_zk)
     self.rev_cache = ZkSharedCache(self.zk, self.ZK_REV_CACHE_PATH,
                                    self._rev_entries_handler)
     self._init_request_logger()
Example #4
0
    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        self.cc_requests = RequestHandler.start(
            "CC Requests",
            self._check_cc,
            self._fetch_cc,
            self._reply_cc,
        )
        self.trc_requests = RequestHandler.start(
            "TRC Requests",
            self._check_trc,
            self._fetch_trc,
            self._reply_trc,
        )
        self.drkey_protocol_requests = RequestHandler.start(
            "DRKey Requests",
            self._check_drkey,
            self._fetch_drkey,
            self._reply_proto_drkey,
        )

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
            },
            PayloadClass.DRKEY: {
                DRKeyMgmtType.FIRST_ORDER_REQUEST: self.process_drkey_request,
                DRKeyMgmtType.FIRST_ORDER_REPLY: self.process_drkey_reply,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.topology.isd_as, CERTIFICATE_SERVICE, zkid,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.trc_cache = ZkSharedCache(self.zk, self.ZK_TRC_CACHE_PATH,
                                       self._cached_trcs_handler)
        self.cc_cache = ZkSharedCache(self.zk, self.ZK_CC_CACHE_PATH,
                                      self._cached_certs_handler)
        self.drkey_cache = ZkSharedCache(self.zk, self.ZK_DRKEY_PATH,
                                         self._cached_drkeys_handler)

        lib_sciond.init(
            os.path.join(SCIOND_API_SOCKDIR, "sd%s.sock" % self.addr.isd_as))
        self.signing_key = get_sig_key(self.conf_dir)
        self.private_key = get_enc_key(self.conf_dir)
        self.public_key = self.private_key.public_key
        self.drkey_secrets = ExpiringDict(DRKEY_MAX_SV, DRKEY_MAX_TTL)
        self.first_order_drkeys = ExpiringDict(DRKEY_MAX_KEYS, DRKEY_MAX_TTL)
Example #5
0
 def __init__(self, api_addr, counter):  # pragma: no cover
     self._api_addr = api_addr
     self._req_id = counter
     self._if_infos = ExpiringDict(100, _IF_INFO_TTL)
     self._svc_infos = ExpiringDict(100, _SVC_INFO_TTL)
     self._as_infos = ExpiringDict(100, _AS_INFO_TTL)
     self._if_infos_lock = threading.Lock()
     self._svc_infos_lock = threading.Lock()
     self._as_infos_lock = threading.Lock()
Example #6
0
 def __init__(self, dns_servers, domain, lifetime=5.0):  # pragma: no cover
     """
     :param list dns_servers:
         DNS server IP addresses as strings. E.g. ``["127.0.0.1",
         "8.8.8.8"]``
     :param string domain: The DNS domain to query.
     :param float lifetime:
         Number of seconds in total to try resolving before failing.
     """
     super().__init__(dns_servers, domain, lifetime=lifetime)
     self.cache = ExpiringDict(max_len=DNS_CACHE_MAX_SIZE,
                               max_age_seconds=DNS_CACHE_MAX_AGE)
Example #7
0
    def __init__(self, server_id, conf_dir, prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param str prom_export: prometheus export address.
        """
        super().__init__(server_id, conf_dir, prom_export=prom_export)
        cc_labels = {**self._labels, "type": "cc"} if self._labels else None
        trc_labels = {**self._labels, "type": "trc"} if self._labels else None
        drkey_labels = {**self._labels, "type": "drkey"} if self._labels else None
        self.cc_requests = RequestHandler.start(
            "CC Requests", self._check_cc, self._fetch_cc, self._reply_cc,
            labels=cc_labels,
        )
        self.trc_requests = RequestHandler.start(
            "TRC Requests", self._check_trc, self._fetch_trc, self._reply_trc,
            labels=trc_labels,
        )
        self.drkey_protocol_requests = RequestHandler.start(
            "DRKey Requests", self._check_drkey, self._fetch_drkey, self._reply_proto_drkey,
            labels=drkey_labels,
        )

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
            },
            PayloadClass.DRKEY: {
                DRKeyMgmtType.FIRST_ORDER_REQUEST:
                    self.process_drkey_request,
                DRKeyMgmtType.FIRST_ORDER_REPLY:
                    self.process_drkey_reply,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.topology.isd_as, CERTIFICATE_SERVICE,
                            zkid, self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.trc_cache = ZkSharedCache(self.zk, self.ZK_TRC_CACHE_PATH,
                                       self._cached_trcs_handler)
        self.cc_cache = ZkSharedCache(self.zk, self.ZK_CC_CACHE_PATH,
                                      self._cached_certs_handler)
        self.drkey_cache = ZkSharedCache(self.zk, self.ZK_DRKEY_PATH,
                                         self._cached_drkeys_handler)
        self.signing_key = get_sig_key(self.conf_dir)
        self.private_key = get_enc_key(self.conf_dir)
        self.drkey_secrets = ExpiringDict(DRKEY_MAX_SV, DRKEY_MAX_TTL)
        self.first_order_drkeys = ExpiringDict(DRKEY_MAX_KEYS, DRKEY_MAX_TTL)
Example #8
0
 def __init__(self, server_id, conf_dir):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     """
     super().__init__(server_id, conf_dir)
     # Sanity check that we should indeed be a core path server.
     assert self.topology.is_core_as, "This shouldn't be a local PS!"
     self._master_id = None  # Address of master core Path Server.
     self._segs_to_master = ExpiringDict(1000, 10)
     self._segs_to_prop = ExpiringDict(1000,
                                       2 * self.config.propagation_time)
Example #9
0
    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        # TODO: add 2 policies
        self.path_policy = PathPolicy.from_file(
            os.path.join(conf_dir, PATH_POLICY_FILE))
        self.unverified_beacons = deque()
        self.trc_requests = {}
        self.trcs = {}
        sig_key_file = get_sig_key_file_path(self.conf_dir)
        self.signing_key = base64.b64decode(read_file(sig_key_file))
        self.of_gen_key = PBKDF2(self.config.master_as_key, b"Derive OF Key")
        logging.info(self.config.__dict__)
        self.if2rev_tokens = {}
        self._if_rev_token_lock = threading.Lock()
        self.revs_to_downstream = ExpiringDict(max_len=1000,
                                               max_age_seconds=60)

        self.ifid_state = {}
        for ifid in self.ifid2er:
            self.ifid_state[ifid] = InterfaceState()

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PCB: {
                PCBType.SEGMENT: self.handle_pcb
            },
            PayloadClass.IFID: {
                IFIDType.PAYLOAD: self.handle_ifid_packet
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_rep,
                CertMgmtType.TRC_REPLY: self.process_trc_rep,
            },
            PayloadClass.PATH: {
                PMT.IFSTATE_REQ: self._handle_ifstate_request
            },
        }

        # Add more IPs here if we support dual-stack
        name_addrs = "\0".join(
            [self.id, str(SCION_UDP_PORT),
             str(self.addr.host)])
        self.zk = Zookeeper(self.addr.isd_as, BEACON_SERVICE, name_addrs,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.incoming_pcbs = deque()
        self.pcb_cache = ZkSharedCache(self.zk, self.ZK_PCB_CACHE_PATH,
                                       self.process_pcbs)
        self.revobjs_cache = ZkSharedCache(self.zk, self.ZK_REVOCATIONS_PATH,
                                           self.process_rev_objects)
Example #10
0
    def __init__(self, conf_dir, addr, api_addr, run_local_api=False,
                 port=None, spki_cache_dir=GEN_CACHE_PATH, prom_export=None, delete_sock=False):
        """
        Initialize an instance of the class SCIONDaemon.
        """
        super().__init__("sciond", conf_dir, spki_cache_dir=spki_cache_dir,
                         prom_export=prom_export, public=[(addr, port)])
        up_labels = {**self._labels, "type": "up"} if self._labels else None
        down_labels = {**self._labels, "type": "down"} if self._labels else None
        core_labels = {**self._labels, "type": "core"} if self._labels else None
        self.up_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=up_labels)
        self.down_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=down_labels)
        self.core_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=core_labels)
        self.rev_cache = RevCache()
        # Keep track of requested paths.
        self.requested_paths = ExpiringDict(self.MAX_REQS, PATH_REQ_TOUT)
        self.req_path_lock = threading.Lock()
        self._api_sock = None
        self.daemon_thread = None
        os.makedirs(SCIOND_API_SOCKDIR, exist_ok=True)
        self.api_addr = (api_addr or get_default_sciond_path())
        if delete_sock:
            try:
                os.remove(self.api_addr)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    logging.error("Could not delete socket %s: %s" % (self.api_addr, e))

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.REPLY: self.handle_path_reply,
                PMT.REVOCATION: self.handle_revocation,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }

        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH:
                {SCMPPathClass.REVOKED_IF: self.handle_scmp_revocation},
        }

        if run_local_api:
            self._api_sock = ReliableSocket(bind_unix=(self.api_addr, "sciond"))
            self._socks.add(self._api_sock, self.handle_accept)
Example #11
0
 def __init__(self, server_id, conf_dir, spki_cache_dir=GEN_CACHE_PATH,
              prom_export=None, sciond_path=None):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     :param str prom_export: prometheus export address.
     :param str sciond_path: path to sciond socket.
     """
     super().__init__(server_id, conf_dir, spki_cache_dir=spki_cache_dir,
                      prom_export=prom_export, sciond_path=sciond_path)
     # Sanity check that we should indeed be a core path server.
     assert self.topology.is_core_as, "This shouldn't be a local PS!"
     self._master_id = None  # Address of master core Path Server.
     self._segs_to_master = ExpiringDict(1000, 10)
     self._segs_to_prop = ExpiringDict(1000, 2 * self.config.propagation_time)
Example #12
0
 def __init__(self, server_id, conf_dir):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     """
     super().__init__(server_id, conf_dir)
     self.down_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
     self.core_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
     self.pending_req = defaultdict(list)  # Dict of pending requests.
     # Used when l/cPS doesn't have up/dw-path.
     self.waiting_targets = defaultdict(list)
     self.revocations = ExpiringDict(1000, 300)
     self.iftoken2seg = defaultdict(set)
     self.CTRL_PLD_CLASS_MAP = {
         PayloadClass.PATH: {
             PMT.REQUEST: self.path_resolution,
             PMT.REPLY: self.handle_path_segment_record,
             PMT.REG: self.handle_path_segment_record,
             PMT.REVOCATION: self._handle_revocation,
             PMT.SYNC: self.handle_path_segment_record,
         },
     }
     self._segs_to_zk = deque()
     # Add more IPs here if we support dual-stack
     name_addrs = "\0".join(
         [self.id, str(SCION_UDP_PORT),
          str(self.addr.host)])
     self.zk = Zookeeper(self.topology.isd_as, PATH_SERVICE, name_addrs,
                         self.topology.zookeepers)
     self.zk.retry("Joining party", self.zk.party_setup)
     self.path_cache = ZkSharedCache(self.zk, self.ZK_PATH_CACHE_PATH,
                                     self._cached_entries_handler)
Example #13
0
 def __init__(
     self,
     server_id,
     conf_dir,
 ):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     """
     super().__init__(
         server_id,
         conf_dir,
     )
     self.interface = None
     for border_router in self.topology.get_all_border_routers():
         if border_router.name == self.id:
             self.interface = border_router.interface
             break
     assert self.interface is not None
     logging.info("Interface: %s", self.interface.__dict__)
     self.is_core_router = self.topology.is_core_as
     self.of_gen_key = kdf(self.config.master_as_key, b"Derive OF Key")
     self.sibra_key = kdf(self.config.master_as_key, b"Derive SIBRA Key")
     self.if_states = defaultdict(InterfaceState)
     self.revocations = ExpiringDict(1000, self.FWD_REVOCATION_TIMEOUT)
     self.pre_ext_handlers = {
         SibraExtBase.EXT_TYPE: self.handle_sibra,
         TracerouteExt.EXT_TYPE: self.handle_traceroute,
         OneHopPathExt.EXT_TYPE: self.handle_one_hop_path,
         ExtHopByHopType.SCMP: self.handle_scmp,
     }
     self.post_ext_handlers = {
         SibraExtBase.EXT_TYPE: False,
         TracerouteExt.EXT_TYPE: False,
         ExtHopByHopType.SCMP: False,
         OneHopPathExt.EXT_TYPE: False,
     }
     self.sibra_state = SibraState(
         self.interface.bandwidth, "%s#%s -> %s" %
         (self.addr.isd_as, self.interface.if_id, self.interface.isd_as))
     self.CTRL_PLD_CLASS_MAP = {
         PayloadClass.IFID: {
             None: self.process_ifid_request
         },
         PayloadClass.PATH:
         defaultdict(lambda: self.process_path_mgmt_packet),
     }
     self.SCMP_PLD_CLASS_MAP = {
         SCMPClass.PATH: {
             SCMPPathClass.REVOKED_IF: self.process_revocation
         },
     }
     self._remote_sock = UDPSocket(
         bind=(str(self.interface.addr), self.interface.udp_port),
         addr_type=self.interface.addr.TYPE,
     )
     self._socks.add(self._remote_sock, self.handle_recv)
     logging.info("IP %s:%d", self.interface.addr, self.interface.udp_port)
Example #14
0
 def __init__(self, server_id, conf_dir):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     """
     super().__init__(server_id, conf_dir)
     self.down_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
     self.core_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
     self.pending_req = defaultdict(list)  # Dict of pending requests.
     # Used when l/cPS doesn't have up/dw-path.
     self.waiting_targets = defaultdict(list)
     self.revocations = RevCache()
     # A mapping from (hash tree root of AS, IFID) to segments
     self.htroot_if2seg = ExpiringDict(1000, HASHTREE_TTL)
     self.htroot_if2seglock = Lock()
     self.CTRL_PLD_CLASS_MAP = {
         PayloadClass.PATH: {
             PMT.REQUEST: self.path_resolution,
             PMT.REPLY: self.handle_path_segment_record,
             PMT.REG: self.handle_path_segment_record,
             PMT.REVOCATION: self._handle_revocation,
             PMT.SYNC: self.handle_path_segment_record,
         },
     }
     self.SCMP_PLD_CLASS_MAP = {
         SCMPClass.PATH: {
             SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
         },
     }
     self._segs_to_zk = deque()
     self._revs_to_zk = deque()
     self._zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                   [(self.addr.host, self._port)])
     self.zk = Zookeeper(self.topology.isd_as, PATH_SERVICE,
                         self._zkid.copy().pack(), self.topology.zookeepers)
     self.zk.retry("Joining party", self.zk.party_setup)
     self.path_cache = ZkSharedCache(self.zk, self.ZK_PATH_CACHE_PATH,
                                     self._cached_entries_handler)
     self.rev_cache = ZkSharedCache(self.zk, self.ZK_REV_CACHE_PATH,
                                    self._rev_entries_handler)
Example #15
0
class DNSCachingClient(DNSClient):
    """
    Caching variant of the DNS client.
    """
    def __init__(self, dns_servers, domain, lifetime=5.0):  # pragma: no cover
        """
        :param list dns_servers:
            DNS server IP addresses as strings. E.g. ``["127.0.0.1",
            "8.8.8.8"]``
        :param string domain: The DNS domain to query.
        :param float lifetime:
            Number of seconds in total to try resolving before failing.
        """
        super().__init__(dns_servers, domain, lifetime=lifetime)
        self.cache = ExpiringDict(max_len=DNS_CACHE_MAX_SIZE,
                                  max_age_seconds=DNS_CACHE_MAX_AGE)

    def query(self, qname, fallback=None, quiet=False):
        """
        Check if the answer is already in the cache. If not, pass it along to
        the DNS client and cache the result.

        :param string qname: A relative DNS record to query. E.g. ``"bs"``
        :param list fallback:
            If provided, and the DNS query fails, use this as the answer
            instead.
        :param bool quiet: If set, don't log warnings/errors.
        :returns: A list of `Host address <HostAddrBase>`_ objects.
        :raises:
            DNSLibTimeout: No responses received.
            DNSLibNxDomain: Name doesn't exist.
            DNSLibError: Unexpected error.
        """
        answer = self.cache.get(qname)
        if answer is None:
            answer = fallback
            try:
                answer = super().query(qname)
            except DNSLibBaseError as e:
                if fallback is None:
                    raise
                if isinstance(e, DNSLibMinorError):
                    level = logging.WARN
                else:
                    level = logging.ERROR
                if not quiet:
                    logging.log(
                        level, "DNS failure, using fallback value for %s: %s",
                        qname, e)
            self.cache[qname] = answer
        shuffle(answer)
        return answer
Example #16
0
class CertServer(SCIONElement):
    """
    The SCION Certificate Server.
    """
    SERVICE_TYPE = CERTIFICATE_SERVICE
    # ZK path for incoming cert chains
    ZK_CC_CACHE_PATH = "cert_chain_cache"
    # ZK path for incoming TRCs
    ZK_TRC_CACHE_PATH = "trc_cache"
    ZK_DRKEY_PATH = "drkey_cache"

    def __init__(self,
                 server_id,
                 conf_dir,
                 spki_cache_dir=GEN_CACHE_PATH,
                 prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param str prom_export: prometheus export address.
        """
        super().__init__(server_id,
                         conf_dir,
                         spki_cache_dir=spki_cache_dir,
                         prom_export=prom_export)
        self.config = self._load_as_conf()
        cc_labels = {**self._labels, "type": "cc"} if self._labels else None
        trc_labels = {**self._labels, "type": "trc"} if self._labels else None
        drkey_labels = {
            **self._labels, "type": "drkey"
        } if self._labels else None
        self.cc_requests = RequestHandler.start(
            "CC Requests",
            self._check_cc,
            self._fetch_cc,
            self._reply_cc,
            labels=cc_labels,
        )
        self.trc_requests = RequestHandler.start(
            "TRC Requests",
            self._check_trc,
            self._fetch_trc,
            self._reply_trc,
            labels=trc_labels,
        )
        self.drkey_protocol_requests = RequestHandler.start(
            "DRKey Requests",
            self._check_drkey,
            self._fetch_drkey,
            self._reply_proto_drkey,
            labels=drkey_labels,
        )

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
            },
            PayloadClass.DRKEY: {
                DRKeyMgmtType.FIRST_ORDER_REQUEST: self.process_drkey_request,
                DRKeyMgmtType.FIRST_ORDER_REPLY: self.process_drkey_reply,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.topology.isd_as, CERTIFICATE_SERVICE, zkid,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.trc_cache = ZkSharedCache(self.zk, self.ZK_TRC_CACHE_PATH,
                                       self._cached_trcs_handler)
        self.cc_cache = ZkSharedCache(self.zk, self.ZK_CC_CACHE_PATH,
                                      self._cached_certs_handler)
        self.drkey_cache = ZkSharedCache(self.zk, self.ZK_DRKEY_PATH,
                                         self._cached_drkeys_handler)
        self.signing_key = get_sig_key(self.conf_dir)
        self.private_key = get_enc_key(self.conf_dir)
        self.drkey_secrets = ExpiringDict(DRKEY_MAX_SV, DRKEY_MAX_TTL)
        self.first_order_drkeys = ExpiringDict(DRKEY_MAX_KEYS, DRKEY_MAX_TTL)

    def worker(self):
        """
        Worker thread that takes care of reading shared entries from ZK, and
        handling master election.
        """
        worker_cycle = 1.0
        start = SCIONTime.get_time()
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "CS.worker cycle",
                           self._quiet_startup())
            start = SCIONTime.get_time()
            # Update IS_MASTER metric.
            if self._labels:
                IS_MASTER.labels(**self._labels).set(int(self.zk.have_lock()))
            try:
                self.zk.wait_connected()
                self.trc_cache.process()
                self.cc_cache.process()
                self.drkey_cache.process()
                # Try to become a master.
                ret = self.zk.get_lock(lock_timeout=0, conn_timeout=0)
                if ret:  # Either got the lock, or already had it.
                    if ret == ZK_LOCK_SUCCESS:
                        logging.info("Became master")
                    self.trc_cache.expire(worker_cycle * 10)
                    self.cc_cache.expire(worker_cycle * 10)
                    self.drkey_cache.expire(worker_cycle * 10)
            except ZkNoConnection:
                logging.warning('worker(): ZkNoConnection')
                pass

    def _cached_trcs_handler(self, raw_entries):
        """
        Handles cached (through ZK) TRCs, passed as a list.
        """
        for raw in raw_entries:
            trc = TRC.from_raw(raw.decode('utf-8'))
            rep = CtrlPayload(CertMgmt(TRCReply.from_values(trc)))
            self.process_trc_reply(rep, None, from_zk=True)
        if len(raw_entries) > 0:
            logging.debug("Processed %s trcs from ZK", len(raw_entries))

    def _cached_certs_handler(self, raw_entries):
        """
        Handles cached (through ZK) chains, passed as a list.
        """
        for raw in raw_entries:
            cert = CertificateChain.from_raw(raw.decode('utf-8'))
            rep = CtrlPayload(CertMgmt(CertChainReply.from_values(cert)))
            self.process_cert_chain_reply(rep, None, from_zk=True)
        if len(raw_entries) > 0:
            logging.debug("Processed %s certs from ZK", len(raw_entries))

    def _cached_drkeys_handler(self, raw_entries):
        for raw in raw_entries:
            msg = CtrlPayload(DRKeyMgmt(DRKeyReply.from_raw(raw)))
            self.process_drkey_reply(msg, None, from_zk=True)

    def _share_object(self, pld, is_trc):
        """
        Share path segments (via ZK) with other path servers.
        """
        pld_packed = pld.pack()
        pld_hash = crypto_hash(pld_packed).hex()
        try:
            if is_trc:
                self.trc_cache.store(
                    "%s-%s" % (pld_hash, SCIONTime.get_time()), pld_packed)
            else:
                self.cc_cache.store("%s-%s" % (pld_hash, SCIONTime.get_time()),
                                    pld_packed)
        except ZkNoConnection:
            logging.warning("Unable to store %s in shared path: "
                            "no connection to ZK" % "TRC" if is_trc else "CC")
            return
        logging.debug("%s stored in ZK: %s" %
                      ("TRC" if is_trc else "CC", pld_hash))

    def process_cert_chain_request(self, cpld, meta):
        """Process a certificate chain request."""
        cmgt = cpld.union
        req = cmgt.union
        assert isinstance(req, CertChainRequest), type(req)
        key = req.isd_as(), req.p.version
        logging.info("Cert chain request received for %sv%s from %s", *key,
                     meta)
        REQS_TOTAL.labels(**self._labels, type="cc").inc()
        local = meta.ia == self.addr.isd_as
        if not self._check_cc(key):
            if not local:
                logging.warning(
                    "Dropping CC request from %s for %sv%s: "
                    "CC not found && requester is not local)", meta, *key)
            else:
                self.cc_requests.put((key, (meta, req, cpld.req_id)))
            return
        self._reply_cc(key, (meta, req, cpld.req_id))

    def process_cert_chain_reply(self, cpld, meta, from_zk=False):
        """Process a certificate chain reply."""
        cmgt = cpld.union
        rep = cmgt.union
        assert isinstance(rep, CertChainReply), type(rep)
        ia_ver = rep.chain.get_leaf_isd_as_ver()
        logging.info("Cert chain reply received for %sv%s (ZK: %s)" %
                     (ia_ver[0], ia_ver[1], from_zk))
        self.trust_store.add_cert(rep.chain)
        if not from_zk:
            self._share_object(rep.chain, is_trc=False)
        # Reply to all requests for this certificate chain
        self.cc_requests.put((ia_ver, None))

    def _check_cc(self, key):
        isd_as, ver = key
        ver = None if ver == CertChainRequest.NEWEST_VERSION else ver
        cert_chain = self.trust_store.get_cert(isd_as, ver)
        if cert_chain:
            return True
        logging.debug('Cert chain not found for %sv%s', *key)
        return False

    def _fetch_cc(self, key, req_info):
        # Do not attempt to fetch the CertChain from a remote AS if the cacheOnly flag is set.
        _, orig_req, _ = req_info
        if orig_req.p.cacheOnly:
            return
        self._send_cc_request(*key)

    def _send_cc_request(self, isd_as, ver):
        req = CertChainRequest.from_values(isd_as, ver, cache_only=True)
        path_meta = self._get_path_via_sciond(isd_as)
        if path_meta:
            meta = self._build_meta(isd_as,
                                    host=SVCType.CS_A,
                                    path=path_meta.fwd_path())
            req_id = mk_ctrl_req_id()
            self.send_meta(CtrlPayload(CertMgmt(req), req_id=req_id), meta)
            logging.info(
                "Cert chain request sent to %s via [%s]: %s [id: %016x]", meta,
                path_meta.short_desc(), req.short_desc(), req_id)
        else:
            logging.warning(
                "Cert chain request (for %s) not sent: "
                "no path found", req.short_desc())

    def _reply_cc(self, key, req_info):
        isd_as, ver = key
        ver = None if ver == CertChainRequest.NEWEST_VERSION else ver
        meta = req_info[0]
        req_id = req_info[2]
        cert_chain = self.trust_store.get_cert(isd_as, ver)
        self.send_meta(
            CtrlPayload(CertMgmt(CertChainReply.from_values(cert_chain)),
                        req_id=req_id), meta)
        logging.info("Cert chain for %sv%s sent to %s [id: %016x]", isd_as,
                     ver, meta, req_id)

    def process_trc_request(self, cpld, meta):
        """Process a TRC request."""
        cmgt = cpld.union
        req = cmgt.union
        assert isinstance(req, TRCRequest), type(req)
        key = req.isd_as()[0], req.p.version
        logging.info("TRC request received for %sv%s from %s [id: %s]", *key,
                     meta, cpld.req_id_str())
        REQS_TOTAL.labels(**self._labels, type="trc").inc()
        local = meta.ia == self.addr.isd_as
        if not self._check_trc(key):
            if not local:
                logging.warning(
                    "Dropping TRC request from %s for %sv%s: "
                    "TRC not found && requester is not local)", meta, *key)
            else:
                self.trc_requests.put((key, (meta, req, cpld.req_id)))
            return
        self._reply_trc(key, (meta, req, cpld.req_id))

    def process_trc_reply(self, cpld, meta, from_zk=False):
        """
        Process a TRC reply.

        :param trc_rep: TRC reply.
        :type trc_rep: TRCReply
        """
        cmgt = cpld.union
        trc_rep = cmgt.union
        assert isinstance(trc_rep, TRCReply), type(trc_rep)
        isd, ver = trc_rep.trc.get_isd_ver()
        logging.info("TRCReply received for ISD %sv%s, ZK: %s [id: %s]", isd,
                     ver, from_zk, cpld.req_id_str())
        self.trust_store.add_trc(trc_rep.trc)
        if not from_zk:
            self._share_object(trc_rep.trc, is_trc=True)
        # Reply to all requests for this TRC
        self.trc_requests.put(((isd, ver), None))

    def _check_trc(self, key):
        isd, ver = key
        ver = None if ver == TRCRequest.NEWEST_VERSION else ver
        trc = self.trust_store.get_trc(isd, ver)
        if trc:
            return True
        logging.debug('TRC not found for %sv%s', *key)
        return False

    def _fetch_trc(self, key, req_info):
        # Do not attempt to fetch the TRC from a remote AS if the cacheOnly flag is set.
        _, orig_req, _ = req_info
        if orig_req.p.cacheOnly:
            return
        self._send_trc_request(*key)

    def _send_trc_request(self, isd, ver):
        trc_req = TRCRequest.from_values(isd, ver, cache_only=True)
        path_meta = self._get_path_via_sciond(trc_req.isd_as())
        if path_meta:
            meta = self._build_meta(path_meta.dst_ia(),
                                    host=SVCType.CS_A,
                                    path=path_meta.fwd_path())
            req_id = mk_ctrl_req_id()
            self.send_meta(CtrlPayload(CertMgmt(trc_req), req_id=req_id), meta)
            logging.info("TRC request sent to %s via [%s]: %s [id: %016x]",
                         meta, path_meta.short_desc(), trc_req.short_desc(),
                         req_id)
        else:
            logging.warning("TRC request not sent for %s: no path found.",
                            trc_req.short_desc())

    def _reply_trc(self, key, req_info):
        isd, ver = key
        ver = None if ver == TRCRequest.NEWEST_VERSION else ver
        meta = req_info[0]
        req_id = req_info[2]
        trc = self.trust_store.get_trc(isd, ver)
        self.send_meta(
            CtrlPayload(CertMgmt(TRCReply.from_values(trc)), req_id=req_id),
            meta)
        logging.info("TRC for %sv%s sent to %s [id: %016x]", isd, ver, meta,
                     req_id)

    def process_drkey_request(self, cpld, meta):
        """
        Process first order DRKey requests from other ASes.

        :param DRKeyRequest req: the DRKey request
        :param UDPMetadata meta: the metadata
        """
        dpld = cpld.union
        req = dpld.union
        assert isinstance(req, DRKeyRequest), type(req)
        logging.info("DRKeyRequest received from %s: %s [id: %s]", meta,
                     req.short_desc(), cpld.req_id_str())
        REQS_TOTAL.labels(**self._labels, type="drkey").inc()
        try:
            cert = self._verify_drkey_request(req, meta)
        except SCIONVerificationError as e:
            logging.warning("Invalid DRKeyRequest from %s. Reason %s: %s",
                            meta, e, req.short_desc())
            return
        sv = self._get_drkey_secret(get_drkey_exp_time(req.p.flags.prefetch))
        cert_version = self.trust_store.get_cert(
            self.addr.isd_as).certs[0].version
        trc_version = self.trust_store.get_trc(self.addr.isd_as[0]).version
        rep = get_drkey_reply(sv, self.addr.isd_as, meta.ia, self.private_key,
                              self.signing_key, cert_version, cert,
                              trc_version)
        self.send_meta(CtrlPayload(DRKeyMgmt(rep), req_id=cpld.req_id), meta)
        logging.info("DRKeyReply sent to %s: %s [id: %s]", meta,
                     req.short_desc(), cpld.req_id_str())

    def _verify_drkey_request(self, req, meta):
        """
        Verify that the first order DRKey request is legit.
        I.e. the signature is valid, the correct ISD AS is queried, timestamp is recent.

        :param DRKeyRequest req: the first order DRKey request.
        :param UDPMetadata meta: the metadata.
        :returns Certificate of the requester.
        :rtype: Certificate
        :raises: SCIONVerificationError
        """
        if self.addr.isd_as != req.isd_as:
            raise SCIONVerificationError("Request for other ISD-AS: %s" %
                                         req.isd_as)
        if drkey_time() - req.p.timestamp > DRKEY_REQUEST_TIMEOUT:
            raise SCIONVerificationError(
                "Expired request from %s. %ss old. Max %ss" %
                (meta.ia, drkey_time() - req.p.timestamp,
                 DRKEY_REQUEST_TIMEOUT))
        trc = self.trust_store.get_trc(meta.ia[0])
        chain = self.trust_store.get_cert(meta.ia, req.p.certVer)
        err = []
        if not chain:
            self._send_cc_request(meta.ia, req.p.certVer)
            err.append("Certificate not present for %s(v: %s)" %
                       (meta.ia, req.p.certVer))
        if not trc:
            self._send_trc_request(meta.ia[0], req.p.trcVer)
            err.append("TRC not present for %s(v: %s)" %
                       (meta.ia[0], req.p.trcVer))
        if err:
            raise SCIONVerificationError(", ".join(err))
        raw = drkey_signing_input_req(req.isd_as, req.p.flags.prefetch,
                                      req.p.timestamp)
        try:
            verify_sig_chain_trc(raw, req.p.signature, meta.ia, chain, trc)
        except SCIONVerificationError as e:
            raise SCIONVerificationError(str(e))
        return chain.certs[0]

    def process_drkey_reply(self, cpld, meta, from_zk=False):
        """
        Process first order DRKey reply from other ASes.

        :param DRKeyReply rep: the received DRKey reply
        :param UDPMetadata meta: the metadata
        :param Bool from_zk: if the reply has been received from Zookeeper
        """
        dpld = cpld.union
        rep = dpld.union
        assert isinstance(rep, DRKeyReply), type(rep)
        logging.info("DRKeyReply received from %s: %s [id: %s]", meta,
                     rep.short_desc(), cpld.req_id_str())
        src = meta or "ZK"

        try:
            cert = self._verify_drkey_reply(rep, meta)
            raw = decrypt_drkey(rep.p.cipher, self.private_key,
                                cert.subject_enc_key_raw)
        except SCIONVerificationError as e:
            logging.info("Invalid DRKeyReply from %s. Reason %s: %s", src, e,
                         rep.short_desc())
            return
        except CryptoError as e:
            logging.info("Unable to decrypt DRKeyReply from %s. Reason %s: %s",
                         src, e, rep.short_desc())
            return
        drkey = FirstOrderDRKey(rep.isd_as, self.addr.isd_as, rep.p.expTime,
                                raw)
        self.first_order_drkeys[drkey] = drkey
        if not from_zk:
            pld_packed = rep.copy().pack()
            try:
                self.drkey_cache.store("%s-%s" % (rep.isd_as, rep.p.expTime),
                                       pld_packed)
            except ZkNoConnection:
                logging.warning("Unable to store DRKey for %s in shared path: "
                                "no connection to ZK" % rep.isd_as)
                return
        self.drkey_protocol_requests.put((drkey, None))

    def _verify_drkey_reply(self, rep, meta):
        """
        Verify that the first order DRKey reply is legit.
        I.e. the signature matches, timestamp is recent.

        :param DRKeyReply rep: the first order DRKey reply.
        :param UDPMetadata meta: the metadata.
        :returns Certificate of the responder.
        :rtype: Certificate
        :raises: SCIONVerificationError
        """
        if meta and meta.ia != rep.isd_as:
            raise SCIONVerificationError("Response from other ISD-AS: %s" %
                                         rep.isd_as)
        if drkey_time() - rep.p.timestamp > DRKEY_REQUEST_TIMEOUT:
            raise SCIONVerificationError(
                "Expired reply from %s. %ss old. Max %ss" %
                (rep.isd_as, drkey_time() - rep.p.timestamp,
                 DRKEY_REQUEST_TIMEOUT))
        trc = self.trust_store.get_trc(rep.isd_as[0])
        chain = self.trust_store.get_cert(rep.isd_as, rep.p.certVerSrc)
        err = []
        if not chain:
            self._send_cc_request(rep.isd_as, rep.p.certVerSrc)
            err.append("Certificate not present for %s(v: %s)" %
                       (rep.isd_as, rep.p.certVerSrc))
        if not trc:
            self._send_trc_request(rep.isd_as[0], rep.p.trcVer)
            err.append("TRC not present for %s(v: %s)" %
                       (rep.isd_as[0], rep.p.trcVer))
        if err:
            raise SCIONVerificationError(", ".join(err))
        raw = get_signing_input_rep(rep.isd_as, rep.p.timestamp, rep.p.expTime,
                                    rep.p.cipher)
        try:
            verify_sig_chain_trc(raw, rep.p.signature, rep.isd_as, chain, trc)
        except SCIONVerificationError as e:
            raise SCIONVerificationError(str(e))
        return chain.certs[0]

    def _check_drkey(self, drkey):
        """
        Check if first order DRKey with the same (SrcIA, DstIA, expTime)
        is available.

        :param FirstOrderDRKey drkey: the searched DRKey.
        :returns: if the the first order DRKey is available.
        :rtype: Bool
        """
        if drkey in self.first_order_drkeys:
            return True
        return False

    def _fetch_drkey(self, drkey, _):
        """
        Fetch missing first order DRKey with the same (SrcIA, DstIA, expTime).

        :param FirstOrderDRKey drkey: The missing DRKey.
        """
        cert = self.trust_store.get_cert(self.addr.isd_as)
        trc = self.trust_store.get_trc(self.addr.isd_as[0])
        if not cert or not trc:
            logging.warning(
                "DRKeyRequest for %s not sent. Own CertChain/TRC not present.",
                drkey.src_ia)
            return
        req = get_drkey_request(drkey.src_ia, False, self.signing_key,
                                cert.certs[0].version, trc.version)
        path_meta = self._get_path_via_sciond(drkey.src_ia)
        if path_meta:
            meta = self._build_meta(drkey.src_ia,
                                    host=SVCType.CS_A,
                                    path=path_meta.fwd_path())
            req_id = mk_ctrl_req_id()
            self.send_meta(CtrlPayload(DRKeyMgmt(req)), meta)
            logging.info("DRKeyRequest (%s) sent to %s via %s [id: %016x]",
                         req.short_desc(), meta, path_meta, req_id)
        else:
            logging.warning("DRKeyRequest (for %s) not sent", req.short_desc())

    def _reply_proto_drkey(self, drkey, meta):
        pass  # TODO(roosd): implement in future PR

    def _get_drkey_secret(self, exp_time):
        """
        Get the drkey secret. A new secret is initialized if no secret is found.

        :param int exp_time: expiration time of the drkey secret
        :return: the according drkey secret
        :rtype: DRKeySecretValue
        """
        sv = self.drkey_secrets.get(exp_time)
        if not sv:
            sv = DRKeySecretValue(
                kdf(self.config.master_as_key, b"Derive DRKey Key"), exp_time)
            self.drkey_secrets[sv.exp_time] = sv
        return sv

    def _init_metrics(self):
        super()._init_metrics()
        for type_ in ("trc", "cc", "drkey"):
            REQS_TOTAL.labels(**self._labels, type=type_).inc(0)
        IS_MASTER.labels(**self._labels).set(0)

    def run(self):
        """
        Run an instance of the Cert Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="CS.worker",
                         daemon=True).start()
        super().run()
Example #17
0
class PathServer(SCIONElement, metaclass=ABCMeta):
    """
    The SCION Path Server.
    """
    SERVICE_TYPE = PATH_SERVICE
    MAX_SEG_NO = 5  # TODO: replace by config variable.
    # ZK path for incoming PATHs
    ZK_PATH_CACHE_PATH = "path_cache"
    # ZK path for incoming REVs
    ZK_REV_CACHE_PATH = "rev_cache"
    # Max number of segments per propagation packet
    PROP_LIMIT = 5
    # Max number of segments per ZK cache entry
    ZK_SHARE_LIMIT = 10
    # Time to store revocations in zookeeper
    ZK_REV_OBJ_MAX_AGE = HASHTREE_EPOCH_TIME
    # TTL of segments in the queue for ZK (in seconds)
    SEGS_TO_ZK_TTL = 10 * 60

    def __init__(self,
                 server_id,
                 conf_dir,
                 spki_cache_dir=GEN_CACHE_PATH,
                 prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param str prom_export: prometheus export address.
        """
        super().__init__(server_id,
                         conf_dir,
                         spki_cache_dir=spki_cache_dir,
                         prom_export=prom_export)
        self.config = self._load_as_conf()
        down_labels = {
            **self._labels, "type": "down"
        } if self._labels else None
        core_labels = {
            **self._labels, "type": "core"
        } if self._labels else None
        self.down_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO,
                                           labels=down_labels)
        self.core_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO,
                                           labels=core_labels)
        # Dict of pending requests.
        self.pending_req = defaultdict(
            lambda: ExpiringDict(1000, PATH_REQ_TOUT))
        self.pen_req_lock = threading.Lock()
        self._request_logger = None
        # Used when l/cPS doesn't have up/dw-path.
        self.waiting_targets = defaultdict(list)
        self.revocations = RevCache(labels=self._labels)
        # A mapping from (hash tree root of AS, IFID) to segments
        self.htroot_if2seg = ExpiringDict(1000,
                                          self.config.revocation_tree_ttl)
        self.htroot_if2seglock = Lock()
        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.IFSTATE_INFOS: self.handle_ifstate_infos,
                PMT.REQUEST: self.path_resolution,
                PMT.REPLY: self.handle_path_reply,
                PMT.REG: self.handle_seg_recs,
                PMT.REVOCATION: self._handle_revocation,
                PMT.SYNC: self.handle_seg_recs,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }
        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
            },
        }
        self._segs_to_zk = ExpiringDict(1000, self.SEGS_TO_ZK_TTL)
        self._revs_to_zk = ExpiringDict(1000, HASHTREE_EPOCH_TIME)
        self._zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                      [(self.addr.host, self._port)])
        self.zk = Zookeeper(self.topology.isd_as, PATH_SERVICE,
                            self._zkid.copy().pack(), self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.path_cache = ZkSharedCache(self.zk, self.ZK_PATH_CACHE_PATH,
                                        self._handle_paths_from_zk)
        self.rev_cache = ZkSharedCache(self.zk, self.ZK_REV_CACHE_PATH,
                                       self._rev_entries_handler)
        self._init_request_logger()

    def worker(self):
        """
        Worker thread that takes care of reading shared paths from ZK, and
        handling master election for core servers.
        """
        worker_cycle = 1.0
        start = SCIONTime.get_time()
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "cPS.worker cycle",
                           self._quiet_startup())
            start = SCIONTime.get_time()
            try:
                self.zk.wait_connected()
                self.path_cache.process()
                self.rev_cache.process()
                # Try to become a master.
                ret = self.zk.get_lock(lock_timeout=0, conn_timeout=0)
                if ret:  # Either got the lock, or already had it.
                    if ret == ZK_LOCK_SUCCESS:
                        logging.info("Became master")
                    self.path_cache.expire(self.config.propagation_time * 10)
                    self.rev_cache.expire(self.ZK_REV_OBJ_MAX_AGE)
            except ZkNoConnection:
                logging.warning('worker(): ZkNoConnection')
                pass
            self._update_master()
            self._propagate_and_sync()
            self._handle_pending_requests()
            self._update_metrics()

    def _update_master(self):
        pass

    def _rev_entries_handler(self, raw_entries):
        for raw in raw_entries:
            rev_info = RevocationInfo.from_raw(raw)
            try:
                rev_info.validate()
            except SCIONBaseError as e:
                logging.warning("Failed to validate RevInfo from zk: %s\n%s",
                                e, rev_info.short_desc())
                continue
            self._remove_revoked_segments(rev_info)

    def _add_rev_mappings(self, pcb):
        """
        Add if revocation token to segment ID mappings.
        """
        segment_id = pcb.get_hops_hash()
        with self.htroot_if2seglock:
            for asm in pcb.iter_asms():
                hof = asm.pcbm(0).hof()
                egress_h = (asm.p.hashTreeRoot, hof.egress_if)
                self.htroot_if2seg.setdefault(egress_h, set()).add(segment_id)
                ingress_h = (asm.p.hashTreeRoot, hof.ingress_if)
                self.htroot_if2seg.setdefault(ingress_h, set()).add(segment_id)

    @abstractmethod
    def _handle_up_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    @abstractmethod
    def _handle_down_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    @abstractmethod
    def _handle_core_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    def _add_segment(self, pcb, seg_db, name, reverse=False):
        res = seg_db.update(pcb, reverse=reverse)
        if res == DBResult.ENTRY_ADDED:
            self._add_rev_mappings(pcb)
            logging.info("%s-Segment registered: %s", name, pcb.short_id())
            return True
        elif res == DBResult.ENTRY_UPDATED:
            self._add_rev_mappings(pcb)
            logging.debug("%s-Segment updated: %s", name, pcb.short_id())
        return False

    def handle_ifstate_infos(self, cpld, meta):
        """
        Handles IFStateInfos.

        :param IFStatePayload infos: The state info objects.
        """
        pmgt = cpld.union
        infos = pmgt.union
        assert isinstance(infos, IFStatePayload), type(infos)
        for info in infos.iter_infos():
            if not info.p.active and info.p.revInfo:
                rev_info = info.rev_info()
                try:
                    rev_info.validate()
                except SCIONBaseError as e:
                    logging.warning(
                        "Failed to validate IFStateInfo RevInfo from %s: %s\n%s",
                        meta, e, rev_info.short_desc())
                    continue
                self._handle_revocation(CtrlPayload(PathMgmt(info.rev_info())),
                                        meta)

    def _handle_scmp_revocation(self, pld, meta):
        rev_info = RevocationInfo.from_raw(pld.info.rev_info)
        try:
            rev_info.validate()
        except SCIONBaseError as e:
            logging.warning("Failed to validate SCMP RevInfo from %s: %s\n%s",
                            meta, e, rev_info.short_desc())
            return
        self._handle_revocation(CtrlPayload(PathMgmt(rev_info)), meta)

    def _handle_revocation(self, cpld, meta):
        """
        Handles a revocation of a segment, interface or hop.

        :param rev_info: The RevocationInfo object.
        """
        pmgt = cpld.union
        rev_info = pmgt.union
        assert isinstance(rev_info, RevocationInfo), type(rev_info)
        # Validate before checking for presense in self.revocations, as that will trigger an assert
        # failure if the rev_info is invalid.
        try:
            rev_info.validate()
        except SCIONBaseError as e:
            # Validation already done in the IFStateInfo and SCMP paths, so a failure here means
            # it's from a CtrlPld.
            logging.warning(
                "Failed to validate CtrlPld RevInfo from %s: %s\n%s", meta, e,
                rev_info.short_desc())
            return

        if rev_info in self.revocations:
            return
        logging.debug("Received revocation from %s: %s", meta,
                      rev_info.short_desc())
        try:
            rev_info.validate()
        except SCIONBaseError as e:
            logging.warning("Failed to validate RevInfo from %s: %s", meta, e)
            return
        if meta.ia[0] != self.addr.isd_as[0]:
            logging.info(
                "Dropping revocation received from a different ISD. Src: %s RevInfo: %s"
                % (meta, rev_info.short_desc()))
            return
        self.revocations.add(rev_info)
        self._revs_to_zk[rev_info] = rev_info.copy().pack(
        )  # have to pack copy
        # Remove segments that contain the revoked interface.
        self._remove_revoked_segments(rev_info)
        # Forward revocation to other path servers.
        self._forward_revocation(rev_info, meta)

    def _remove_revoked_segments(self, rev_info):
        """
        Try the previous and next hashes as possible astokens,
        and delete any segment that matches

        :param rev_info: The revocation info
        :type rev_info: RevocationInfo
        """
        if ConnectedHashTree.verify_epoch(
                rev_info.p.epoch) != ConnectedHashTree.EPOCH_OK:
            return
        (hash01, hash12) = ConnectedHashTree.get_possible_hashes(rev_info)
        if_id = rev_info.p.ifID

        with self.htroot_if2seglock:
            down_segs_removed = 0
            core_segs_removed = 0
            up_segs_removed = 0
            for h in (hash01, hash12):
                for sid in self.htroot_if2seg.pop((h, if_id), []):
                    if self.down_segments.delete(
                            sid) == DBResult.ENTRY_DELETED:
                        down_segs_removed += 1
                    if self.core_segments.delete(
                            sid) == DBResult.ENTRY_DELETED:
                        core_segs_removed += 1
                    if not self.topology.is_core_as:
                        if (self.up_segments.delete(sid) ==
                                DBResult.ENTRY_DELETED):
                            up_segs_removed += 1
            logging.debug(
                "Removed segments revoked by [%s]: UP: %d DOWN: %d CORE: %d" %
                (rev_info.short_desc(), up_segs_removed, down_segs_removed,
                 core_segs_removed))

    @abstractmethod
    def _forward_revocation(self, rev_info, meta):
        """
        Forwards a revocation to other path servers that need to be notified.

        :param rev_info: The RevInfo object.
        :param meta: The MessageMeta object.
        """
        raise NotImplementedError

    def _send_path_segments(self,
                            req,
                            req_id,
                            meta,
                            logger,
                            up=None,
                            core=None,
                            down=None):
        """
        Sends path-segments to requester (depending on Path Server's location).
        """
        up = up or set()
        core = core or set()
        down = down or set()
        all_segs = up | core | down
        if not all_segs:
            logger.warning("No segments to send for request: %s from: %s" %
                           (req.short_desc(), meta))
            return
        revs_to_add = self._peer_revs_for_segs(all_segs)
        recs = PathSegmentRecords.from_values(
            {
                PST.UP: up,
                PST.CORE: core,
                PST.DOWN: down
            }, revs_to_add)
        pld = PathSegmentReply.from_values(req.copy(), recs)
        self.send_meta(CtrlPayload(PathMgmt(pld), req_id=req_id), meta)
        logger.info("Sending PATH_REPLY with %d segment(s).", len(all_segs))

    def _peer_revs_for_segs(self, segs):
        """Returns a list of peer revocations for segments in 'segs'."""
        def _handle_one_seg(seg):
            for asm in seg.iter_asms():
                for pcbm in asm.iter_pcbms(1):
                    hof = pcbm.hof()
                    for if_id in [hof.ingress_if, hof.egress_if]:
                        rev_info = self.revocations.get((asm.isd_as(), if_id))
                        if rev_info:
                            revs_to_add.add(rev_info.copy())
                            return

        revs_to_add = set()
        for seg in segs:
            _handle_one_seg(seg)

        return list(revs_to_add)

    def _handle_pending_requests(self):
        rem_keys = []
        # Serve pending requests.
        with self.pen_req_lock:
            for key in self.pending_req:
                for req_key, (req, req_id, meta,
                              logger) in self.pending_req[key].items():
                    if self.path_resolution(CtrlPayload(PathMgmt(req),
                                                        req_id=req_id),
                                            meta,
                                            new_request=False,
                                            logger=logger):
                        meta.close()
                        del self.pending_req[key][req_key]
                if not self.pending_req[key]:
                    rem_keys.append(key)
            for key in rem_keys:
                del self.pending_req[key]

    def _handle_paths_from_zk(self, raw_entries):
        """
        Handles cached paths through ZK, passed as a list.
        """
        for raw in raw_entries:
            recs = PathSegmentRecords.from_raw(raw)
            for type_, pcb in recs.iter_pcbs():
                seg_meta = PathSegMeta(pcb,
                                       self.continue_seg_processing,
                                       type_=type_,
                                       params={'from_zk': True})
                self._process_path_seg(seg_meta)
        if raw_entries:
            logging.debug("Processed %s segments from ZK", len(raw_entries))

    def handle_path_reply(self, cpld, meta):
        pmgt = cpld.union
        reply = pmgt.union
        assert isinstance(reply, PathSegmentReply), type(reply)
        self._handle_seg_recs(reply.recs(), cpld.req_id, meta)

    def handle_seg_recs(self, cpld, meta):
        pmgt = cpld.union
        seg_recs = pmgt.union
        self._handle_seg_recs(seg_recs, cpld.req_id, meta)

    def _handle_seg_recs(self, seg_recs, req_id, meta):
        """
        Handles paths received from the network.
        """
        assert isinstance(seg_recs, PathSegmentRecords), type(seg_recs)
        params = self._dispatch_params(seg_recs, meta)
        # Add revocations for peer interfaces included in the path segments.
        for rev_info in seg_recs.iter_rev_infos():
            self.revocations.add(rev_info)
        # Verify pcbs and process them
        for type_, pcb in seg_recs.iter_pcbs():
            seg_meta = PathSegMeta(pcb, self.continue_seg_processing, meta,
                                   type_, params)
            self._process_path_seg(seg_meta, req_id)

    def continue_seg_processing(self, seg_meta):
        """
        For every path segment(that can be verified) received from the network
        or ZK this function gets called to continue the processing for the
        segment.
        The segment is added to pathdb and pending requests are checked.
        """
        pcb = seg_meta.seg
        logging.debug("Successfully verified PCB %s" % pcb.short_id())
        type_ = seg_meta.type
        params = seg_meta.params
        self.handle_ext(pcb)
        self._dispatch_segment_record(type_, pcb, **params)
        self._handle_pending_requests()

    def handle_ext(self, pcb):
        """
        Handle beacon extensions.
        """
        # Handle PCB extensions:
        for asm in pcb.iter_asms():
            pol = asm.routing_pol_ext()
            if pol:
                self.handle_routing_pol_ext(pol)

    def handle_routing_pol_ext(self, ext):
        # TODO(Sezer): Implement extension handling
        logging.debug("Routing policy extension: %s" % ext)

    def _dispatch_segment_record(self, type_, seg, **kwargs):
        # Check that segment does not contain a revoked interface.
        if not self._validate_segment(seg):
            return
        handle_map = {
            PST.UP: self._handle_up_segment_record,
            PST.CORE: self._handle_core_segment_record,
            PST.DOWN: self._handle_down_segment_record,
        }
        handle_map[type_](seg, **kwargs)

    def _validate_segment(self, seg):
        """
        Check segment for revoked upstream/downstream interfaces.

        :param seg: The PathSegment object.
        :return: False, if the path segment contains a revoked upstream/
            downstream interface (not peer). True otherwise.
        """
        for asm in seg.iter_asms():
            pcbm = asm.pcbm(0)
            for if_id in [pcbm.hof().ingress_if, pcbm.hof().egress_if]:
                rev_info = self.revocations.get((asm.isd_as(), if_id))
                if rev_info:
                    logging.debug(
                        "Found revoked interface (%d, %s) in segment %s." %
                        (rev_info.p.ifID, rev_info.isd_as(), seg.short_desc()))
                    return False
        return True

    def _dispatch_params(self, pld, meta):
        return {}

    def _propagate_and_sync(self):
        self._share_via_zk()
        self._share_revs_via_zk()

    def _gen_prop_recs(self, container, limit=PROP_LIMIT):
        count = 0
        pcbs = defaultdict(list)
        while container:
            try:
                _, (type_, pcb) = container.popitem(last=False)
            except KeyError:
                continue
            count += 1
            pcbs[type_].append(pcb.copy())
            if count >= limit:
                yield (pcbs)
                count = 0
                pcbs = defaultdict(list)
        if pcbs:
            yield (pcbs)

    @abstractmethod
    def path_resolution(self,
                        path_request,
                        meta,
                        new_request=True,
                        logger=None):
        """
        Handles all types of path request.
        """
        raise NotImplementedError

    def _handle_waiting_targets(self, pcb):
        """
        Handle any queries that are waiting for a path to any core AS in an ISD.
        """
        dst_ia = pcb.first_ia()
        if not self.is_core_as(dst_ia):
            logging.warning("Invalid waiting target, not a core AS: %s",
                            dst_ia)
            return
        self._send_waiting_queries(dst_ia[0], pcb)

    def _send_waiting_queries(self, dst_isd, pcb):
        targets = self.waiting_targets[dst_isd]
        if not targets:
            return
        path = pcb.get_path(reverse_direction=True)
        src_ia = pcb.first_ia()
        while targets:
            (seg_req, logger) = targets.pop(0)
            meta = self._build_meta(ia=src_ia,
                                    path=path,
                                    host=SVCType.PS_A,
                                    reuse=True)
            req_id = mk_ctrl_req_id()
            self.send_meta(CtrlPayload(PathMgmt(seg_req), req_id=req_id), meta)
            logger.info("Waiting request (%s) sent to %s via %s [id: %016x]",
                        seg_req.short_desc(), meta, pcb.short_desc(), req_id)

    def _share_via_zk(self):
        if not self._segs_to_zk:
            return
        logging.info("Sharing %d segment(s) via ZK", len(self._segs_to_zk))
        for pcb_dict in self._gen_prop_recs(self._segs_to_zk,
                                            limit=self.ZK_SHARE_LIMIT):
            seg_recs = PathSegmentRecords.from_values(pcb_dict)
            self._zk_write(seg_recs.pack())

    def _share_revs_via_zk(self):
        if not self._revs_to_zk:
            return
        logging.info("Sharing %d revocation(s) via ZK", len(self._revs_to_zk))
        while self._revs_to_zk:
            try:
                data = self._revs_to_zk.popitem(last=False)[1]
            except KeyError:
                continue
            self._zk_write_rev(data)

    def _zk_write(self, data):
        hash_ = crypto_hash(data).hex()
        try:
            self.path_cache.store("%s-%s" % (hash_, SCIONTime.get_time()),
                                  data)
        except ZkNoConnection:
            logging.warning("Unable to store segment(s) in shared path: "
                            "no connection to ZK")

    def _zk_write_rev(self, data):
        hash_ = crypto_hash(data).hex()
        try:
            self.rev_cache.store("%s-%s" % (hash_, SCIONTime.get_time()), data)
        except ZkNoConnection:
            logging.warning("Unable to store revocation(s) in shared path: "
                            "no connection to ZK")

    def _init_request_logger(self):
        """
        Initializes the request logger.
        """
        self._request_logger = logging.getLogger("RequestLogger")
        # Create new formatter to include the request in the log.
        formatter = formatter = Rfc3339Formatter(
            "%(asctime)s [%(levelname)s] (%(threadName)s) %(message)s "
            "{id=%(id)s, from=%(from)s}")
        add_formatter('RequestLogger', formatter)

    def get_request_logger(self, req_id, meta):
        """
        Returns a logger adapter for a request.
        """
        # Create a logger for the request to log with context.
        return logging.LoggerAdapter(self._request_logger, {
            "id": req_id,
            "from": str(meta)
        })

    def _init_metrics(self):
        super()._init_metrics()
        REQS_TOTAL.labels(**self._labels).inc(0)
        REQS_PENDING.labels(**self._labels).set(0)
        SEGS_TO_ZK.labels(**self._labels).set(0)
        REVS_TO_ZK.labels(**self._labels).set(0)
        HT_ROOT_MAPPTINGS.labels(**self._labels).set(0)
        IS_MASTER.labels(**self._labels).set(0)

    def _update_metrics(self):
        """
        Updates all Gauge metrics. Subclass can update their own metrics but must
        call the superclass' implementation.
        """
        if not self._labels:
            return
        # Update pending requests metric.
        # XXX(shitz): This could become a performance problem should there ever be
        # a large amount of pending requests (>100'000).
        total_pending = 0
        with self.pen_req_lock:
            for reqs in self.pending_req.values():
                total_pending += len(reqs)
        REQS_PENDING.labels(**self._labels).set(total_pending)
        # Update SEGS_TO_ZK and REVS_TO_ZK metrics.
        SEGS_TO_ZK.labels(**self._labels).set(len(self._segs_to_zk))
        REVS_TO_ZK.labels(**self._labels).set(len(self._revs_to_zk))
        # Update HT_ROOT_MAPPTINGS metric.
        HT_ROOT_MAPPTINGS.labels(**self._labels).set(len(self.htroot_if2seg))
        # Update IS_MASTER metric.
        IS_MASTER.labels(**self._labels).set(int(self.zk.have_lock()))

    def run(self):
        """
        Run an instance of the Path Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="PS.worker",
                         daemon=True).start()
        threading.Thread(target=thread_safety_net,
                         args=(self._check_trc_cert_reqs, ),
                         name="Elem.check_trc_cert_reqs",
                         daemon=True).start()
        super().run()
Example #18
0
 def __init__(self, server_id, conf_dir, public=None, bind=None, spki_cache_dir=GEN_CACHE_PATH,
              prom_export=None):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     :param list public:
         (host_addr, port) of the element's public address
         (i.e. the address visible to other network elements).
     :param list bind:
         (host_addr, port) of the element's bind address, if any
         (i.e. the address the element uses to identify itself to the local
         operating system, if it differs from the public address due to NAT).
     :param str spki_cache_dir:
         Path for caching TRCs and certificate chains.
     :param str prom_export:
         String of the form 'addr:port' specifying the prometheus endpoint.
         If no string is provided, no metrics are exported.
     """
     self.id = server_id
     self.conf_dir = conf_dir
     self.ifid2br = {}
     self.topology = Topology.from_file(
         os.path.join(self.conf_dir, TOPO_FILE))
     # Labels attached to every exported metric.
     self._labels = {"server_id": self.id, "isd_as": str(self.topology.isd_as)}
     # Must be over-ridden by child classes:
     self.CTRL_PLD_CLASS_MAP = {}
     self.SCMP_PLD_CLASS_MAP = {}
     self.public = public
     self.bind = bind
     if self.SERVICE_TYPE:
         own_config = self.topology.get_own_config(self.SERVICE_TYPE,
                                                   server_id)
         if public is None:
             self.public = own_config.public
         if bind is None:
             self.bind = own_config.bind
     self.init_ifid2br()
     self.trust_store = TrustStore(self.conf_dir, spki_cache_dir, self.id, self._labels)
     self.total_dropped = 0
     self._core_ases = defaultdict(list)  # Mapping ISD_ID->list of core ASes
     self.init_core_ases()
     self.run_flag = threading.Event()
     self.run_flag.set()
     self.stopped_flag = threading.Event()
     self.stopped_flag.clear()
     self._in_buf = queue.Queue(MAX_QUEUE)
     self._socks = SocketMgr()
     self._startup = time.time()
     if self.USE_TCP:
         self._DefaultMeta = TCPMetadata
     else:
         self._DefaultMeta = UDPMetadata
     self.unverified_segs = ExpiringDict(500, 60 * 60)
     self.unv_segs_lock = threading.RLock()
     self.requested_trcs = {}
     self.req_trcs_lock = threading.Lock()
     self.requested_certs = {}
     self.req_certs_lock = threading.Lock()
     # TODO(jonghoonkwon): Fix me to setup sockets for multiple public addresses
     host_addr, self._port = self.public[0]
     self.addr = SCIONAddr.from_values(self.topology.isd_as, host_addr)
     if prom_export:
         self._export_metrics(prom_export)
         self._init_metrics()
     self._setup_sockets(True)
     lib_sciond.init(os.path.join(SCIOND_API_SOCKDIR, "sd%s.sock" % self.addr.isd_as))
Example #19
0
class BeaconServer(SCIONElement, metaclass=ABCMeta):
    """
    The SCION PathConstructionBeacon Server.

    Attributes:
        if2rev_tokens: Contains the currently used revocation token
            hash-chain for each interface.
    """
    SERVICE_TYPE = BEACON_SERVICE
    # Amount of time units a HOF is valid (time unit is EXP_TIME_UNIT).
    HOF_EXP_TIME = 63
    # ZK path for incoming PCBs
    ZK_PCB_CACHE_PATH = "pcb_cache"
    # ZK path for revocations.
    ZK_REVOCATIONS_PATH = "rev_cache"
    # Time revocation objects are cached in memory (in seconds).
    ZK_REV_OBJ_MAX_AGE = HASHTREE_EPOCH_TIME
    # Interval to checked for timed out interfaces.
    IF_TIMEOUT_INTERVAL = 1

    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        # TODO: add 2 policies
        self.path_policy = PathPolicy.from_file(
            os.path.join(conf_dir, PATH_POLICY_FILE))
        self.signing_key = get_sig_key(self.conf_dir)
        self.of_gen_key = kdf(self.config.master_as_key, b"Derive OF Key")
        self.hashtree_gen_key = kdf(self.config.master_as_key,
                                    b"Derive hashtree Key")
        logging.info(self.config.__dict__)
        self._hash_tree = None
        self._hash_tree_lock = Lock()
        self._next_tree = None
        self._init_hash_tree()
        self.ifid_state = {}
        for ifid in self.ifid2br:
            self.ifid_state[ifid] = InterfaceState()
        self.ifid_state_lock = RLock()
        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PCB: {
                None: self.handle_pcb
            },
            PayloadClass.IFID: {
                None: self.handle_ifid_packet
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
            PayloadClass.PATH: {
                PMT.IFSTATE_REQ: self._handle_ifstate_request,
                PMT.REVOCATION: self._handle_revocation,
            },
        }
        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.addr.isd_as, BEACON_SERVICE, zkid,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.pcb_cache = ZkSharedCache(self.zk, self.ZK_PCB_CACHE_PATH,
                                       self._handle_pcbs_from_zk)
        self.revobjs_cache = ZkSharedCache(self.zk, self.ZK_REVOCATIONS_PATH,
                                           self.process_rev_objects)
        self.local_rev_cache = ExpiringDict(
            1000, HASHTREE_EPOCH_TIME + HASHTREE_EPOCH_TOLERANCE)
        self._rev_seg_lock = RLock()

    def _init_hash_tree(self):
        ifs = list(self.ifid2br.keys())
        self._hash_tree = ConnectedHashTree(self.addr.isd_as, ifs,
                                            self.hashtree_gen_key,
                                            HashType.SHA256)

    def _get_ht_proof(self, if_id):
        with self._hash_tree_lock:
            return self._hash_tree.get_proof(if_id)

    def _get_ht_root(self):
        with self._hash_tree_lock:
            return self._hash_tree.get_root()

    def propagate_downstream_pcb(self, pcb):
        """
        Propagates the beacon to all children.

        :param pcb: path segment.
        :type pcb: PathSegment
        """
        propagated_pcbs = defaultdict(list)
        for intf in self.topology.child_interfaces:
            if not intf.to_if_id:
                continue
            new_pcb, meta = self._mk_prop_pcb_meta(pcb.copy(), intf.isd_as,
                                                   intf.if_id)
            if not new_pcb:
                continue
            self.send_meta(new_pcb, meta)
            propagated_pcbs[(intf.isd_as, intf.if_id)].append(pcb.short_id())
        return propagated_pcbs

    def _mk_prop_pcb_meta(self, pcb, dst_ia, egress_if):
        ts = pcb.get_timestamp()
        asm = self._create_asm(pcb.p.ifID, egress_if, ts, pcb.last_hof())
        if not asm:
            return None, None
        pcb.add_asm(asm)
        pcb.sign(self.signing_key)
        one_hop_path = self._create_one_hop_path(egress_if)
        return pcb, self._build_meta(ia=dst_ia,
                                     host=SVCType.BS_A,
                                     path=one_hop_path,
                                     one_hop=True)

    def _create_one_hop_path(self, egress_if):
        ts = int(SCIONTime.get_time())
        info = InfoOpaqueField.from_values(ts, self.addr.isd_as[0], hops=2)
        hf1 = HopOpaqueField.from_values(self.HOF_EXP_TIME, 0, egress_if)
        hf1.set_mac(self.of_gen_key, ts, None)
        # Return a path where second HF is empty.
        return SCIONPath.from_values(info, [hf1, HopOpaqueField()])

    def _mk_if_info(self, if_id):
        """
        Small helper method to make it easier to deal with ingress/egress
        interface being 0 while building ASMarkings.
        """
        d = {"remote_ia": ISD_AS.from_values(0, 0), "remote_if": 0, "mtu": 0}
        if not if_id:
            return d
        br = self.ifid2br[if_id]
        d["remote_ia"] = br.interfaces[if_id].isd_as
        d["remote_if"] = br.interfaces[if_id].to_if_id
        d["mtu"] = br.interfaces[if_id].mtu
        return d

    @abstractmethod
    def handle_pcbs_propagation(self):
        """
        Main loop to propagate received beacons.
        """
        raise NotImplementedError

    def _log_propagations(self, propagated_pcbs):
        for (isd_as, if_id), pcbs in propagated_pcbs.items():
            logging.debug("Propagated %d PCBs to %s via %s (%s)", len(pcbs),
                          isd_as, if_id, ", ".join(pcbs))

    def _handle_pcbs_from_zk(self, pcbs):
        """
        Handles cached pcbs through ZK, passed as a list.
        """
        for pcb in pcbs:
            try:
                pcb = PathSegment.from_raw(pcb)
            except SCIONParseError as e:
                logging.error("Unable to parse raw pcb: %s", e)
                continue
            self.handle_pcb(pcb)
        if pcbs:
            logging.debug("Processed %s PCBs from ZK", len(pcbs))

    def handle_pcb(self, pcb, meta=None):
        """
        Handles pcbs received from the network.
        """
        if meta:
            pcb.p.ifID = meta.path.get_hof().ingress_if
        try:
            self.path_policy.check_filters(pcb)
        except SCIONPathPolicyViolated as e:
            logging.debug("Segment dropped due to path policy: %s\n%s" %
                          (e, pcb.short_desc()))
            return
        if not self._filter_pcb(pcb):
            logging.debug("Segment dropped due to looping: %s" %
                          pcb.short_desc())
            return
        seg_meta = PathSegMeta(pcb, self.continue_seg_processing, meta)
        self._process_path_seg(seg_meta)

    def continue_seg_processing(self, seg_meta):
        """
        For every verified pcb received from the network or ZK
        this function gets called to continue the processing for the pcb.
        """
        pcb = seg_meta.seg
        logging.debug("Successfully verified PCB %s", pcb.short_id())
        if seg_meta.meta:
            # Segment was received from network, not from zk. Share segment
            # with other beacon servers in this AS.
            entry_name = "%s-%s" % (pcb.get_hops_hash(hex=True), time.time())
            try:
                self.pcb_cache.store(entry_name, pcb.copy().pack())
            except ZkNoConnection:
                logging.error("Unable to store PCB in shared cache: "
                              "no connection to ZK")
        self.handle_ext(pcb)
        self._handle_verified_beacon(pcb)

    def _filter_pcb(self, pcb, dst_ia=None):
        return True

    def handle_ext(self, pcb):
        """
        Handle beacon extensions.
        """
        # Handle PCB extensions
        if pcb.is_sibra():
            logging.debug("%s", pcb.sibra_ext)
        for asm in pcb.iter_asms():
            pol = asm.routing_pol_ext()
            if pol:
                self.handle_routing_pol_ext(pol)

    def handle_routing_pol_ext(self, ext):
        # TODO(Sezer): Implement routing policy extension handling
        logging.debug("Routing policy extension: %s" % ext)

    @abstractmethod
    def register_segments(self):
        """
        Registers paths according to the received beacons.
        """
        raise NotImplementedError

    def _log_registrations(self, registrations, seg_type):
        for (dst_meta, dst_type), pcbs in registrations.items():
            logging.debug("Registered %d %s-segments @ %s:%s (%s)", len(pcbs),
                          seg_type, dst_type.upper(), dst_meta,
                          ", ".join(pcbs))

    def _create_asm(self, in_if, out_if, ts, prev_hof):
        pcbms = list(self._create_pcbms(in_if, out_if, ts, prev_hof))
        if not pcbms:
            return None
        chain = self._get_my_cert()
        _, cert_ver = chain.get_leaf_isd_as_ver()
        return ASMarking.from_values(self.addr.isd_as,
                                     self._get_my_trc().version, cert_ver,
                                     pcbms, self._get_ht_root(),
                                     self.topology.mtu)

    def _create_pcbms(self, in_if, out_if, ts, prev_hof):
        up_pcbm = self._create_pcbm(in_if, out_if, ts, prev_hof)
        if not up_pcbm:
            return
        yield up_pcbm
        for intf in sorted(self.topology.peer_interfaces):
            in_if = intf.if_id
            with self.ifid_state_lock:
                if (not self.ifid_state[in_if].is_active()
                        and not self._quiet_startup()):
                    continue
            peer_pcbm = self._create_pcbm(in_if,
                                          out_if,
                                          ts,
                                          up_pcbm.hof(),
                                          xover=True)
            if peer_pcbm:
                yield peer_pcbm

    def _create_pcbm(self, in_if, out_if, ts, prev_hof, xover=False):
        in_info = self._mk_if_info(in_if)
        if in_info["remote_ia"].int() and not in_info["remote_if"]:
            return None
        out_info = self._mk_if_info(out_if)
        if out_info["remote_ia"].int() and not out_info["remote_if"]:
            return None
        hof = HopOpaqueField.from_values(self.HOF_EXP_TIME,
                                         in_if,
                                         out_if,
                                         xover=xover)
        hof.set_mac(self.of_gen_key, ts, prev_hof)
        return PCBMarking.from_values(in_info["remote_ia"],
                                      in_info["remote_if"], in_info["mtu"],
                                      out_info["remote_ia"],
                                      out_info["remote_if"], hof)

    def _terminate_pcb(self, pcb):
        """
        Copies a PCB, terminates it and adds the segment ID.

        Terminating a PCB means adding a opaque field with the egress IF set
        to 0, i.e., there is no AS to forward a packet containing this path
        segment to.
        """
        pcb = pcb.copy()
        asm = self._create_asm(pcb.p.ifID, 0, pcb.get_timestamp(),
                               pcb.last_hof())
        if not asm:
            return None
        pcb.add_asm(asm)
        return pcb

    def handle_ifid_packet(self, pld, meta):
        """
        Update the interface state for the corresponding interface.

        :param pld: The IFIDPayload.
        :type pld: IFIDPayload
        """
        ifid = pld.p.relayIF
        with self.ifid_state_lock:
            if ifid not in self.ifid_state:
                raise SCIONKeyError("Invalid IF %d in IFIDPayload" % ifid)
            br = self.ifid2br[ifid]
            br.interfaces[ifid].to_if_id = pld.p.origIF
            prev_state = self.ifid_state[ifid].update()
            if prev_state == InterfaceState.INACTIVE:
                logging.info("IF %d activated", ifid)
            elif prev_state in [
                    InterfaceState.TIMED_OUT, InterfaceState.REVOKED
            ]:
                logging.info("IF %d came back up.", ifid)
            if not prev_state == InterfaceState.ACTIVE:
                if self.zk.have_lock():
                    # Inform BRs about the interface coming up.
                    state_info = IFStateInfo.from_values(
                        ifid, True, self._get_ht_proof(ifid))
                    pld = IFStatePayload.from_values([state_info])
                    for br in self.topology.border_routers:
                        br_addr, br_port = br.int_addrs[0].public[0]
                        meta = UDPMetadata.from_values(host=br_addr,
                                                       port=br_port)
                        self.send_meta(pld.copy(), meta, (br_addr, br_port))

    def run(self):
        """
        Run an instance of the Beacon Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="BS.worker",
                         daemon=True).start()
        # https://github.com/netsec-ethz/scion/issues/308:
        threading.Thread(target=thread_safety_net,
                         args=(self._handle_if_timeouts, ),
                         name="BS._handle_if_timeouts",
                         daemon=True).start()
        threading.Thread(target=thread_safety_net,
                         args=(self._create_next_tree, ),
                         name="BS._create_next_tree",
                         daemon=True).start()
        threading.Thread(target=thread_safety_net,
                         args=(self._check_trc_cert_reqs, ),
                         name="Elem.check_trc_cert_reqs",
                         daemon=True).start()
        super().run()

    def _create_next_tree(self):
        last_ttl_window = 0
        while self.run_flag.is_set():
            start = time.time()
            cur_ttl_window = ConnectedHashTree.get_ttl_window()
            time_to_sleep = (ConnectedHashTree.get_time_till_next_ttl() -
                             HASHTREE_UPDATE_WINDOW)
            if cur_ttl_window == last_ttl_window:
                time_to_sleep += HASHTREE_TTL
            if time_to_sleep > 0:
                sleep_interval(start, time_to_sleep, "BS._create_next_tree",
                               self._quiet_startup())

            # at this point, there should be <= HASHTREE_UPDATE_WINDOW
            # seconds left in current ttl
            logging.info("Started computing hashtree for next TTL window (%d)",
                         cur_ttl_window + 2)
            last_ttl_window = ConnectedHashTree.get_ttl_window()

            ht_start = time.time()
            ifs = list(self.ifid2br.keys())
            tree = ConnectedHashTree.get_next_tree(self.addr.isd_as, ifs,
                                                   self.hashtree_gen_key,
                                                   HashType.SHA256)
            ht_end = time.time()
            with self._hash_tree_lock:
                self._next_tree = tree
            logging.info(
                "Finished computing hashtree for TTL window %d in %.3fs" %
                (cur_ttl_window + 2, ht_end - ht_start))

    def _maintain_hash_tree(self):
        """
        Maintain the hashtree. Update the the windows in the connected tree
        """
        with self._hash_tree_lock:
            if self._next_tree is not None:
                self._hash_tree.update(self._next_tree)
                self._next_tree = None
            else:
                logging.critical("Did not create hashtree in time; dying")
                kill_self()
        logging.info("New Hash Tree TTL window beginning: %s",
                     ConnectedHashTree.get_ttl_window())

    def worker(self):
        """
        Worker thread that takes care of reading shared PCBs from ZK, and
        propagating PCBS/registering paths when master.
        """
        last_propagation = last_registration = 0
        last_ttl_window = ConnectedHashTree.get_ttl_window()
        worker_cycle = 1.0
        start = time.time()
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "BS.worker cycle",
                           self._quiet_startup())
            start = time.time()
            try:
                self.zk.wait_connected()
                self.pcb_cache.process()
                self.revobjs_cache.process()
                self.handle_rev_objs()

                cur_ttl_window = ConnectedHashTree.get_ttl_window()
                if cur_ttl_window != last_ttl_window:
                    self._maintain_hash_tree()
                    last_ttl_window = cur_ttl_window

                ret = self.zk.get_lock(lock_timeout=0, conn_timeout=0)
                if not ret:  # Failed to get the lock
                    continue
                elif ret == ZK_LOCK_SUCCESS:
                    logging.info("Became master")
                    self._became_master()
                self.pcb_cache.expire(self.config.propagation_time * 10)
                self.revobjs_cache.expire(self.ZK_REV_OBJ_MAX_AGE)
            except ZkNoConnection:
                continue
            now = time.time()
            if now - last_propagation >= self.config.propagation_time:
                self.handle_pcbs_propagation()
                last_propagation = now
            if (self.config.registers_paths and
                    now - last_registration >= self.config.registration_time):
                try:
                    self.register_segments()
                except SCIONKeyError as e:
                    logging.error("Error while registering segments: %s", e)
                    pass
                last_registration = now

    def _became_master(self):
        """
        Called when a BS becomes the new master. Resets some state that will be
        rebuilt over time.
        """
        # Reset all timed-out and revoked interfaces to inactive.
        with self.ifid_state_lock:
            for (_, ifstate) in self.ifid_state.items():
                if not ifstate.is_active():
                    ifstate.reset()

    def _get_my_trc(self):
        return self.trust_store.get_trc(self.addr.isd_as[0])

    def _get_my_cert(self):
        return self.trust_store.get_cert(self.addr.isd_as)

    @abstractmethod
    def _handle_verified_beacon(self, pcb):
        """
        Once a beacon has been verified, place it into the right containers.

        :param pcb: verified path segment.
        :type pcb: PathSegment
        """
        raise NotImplementedError

    def process_rev_objects(self, rev_infos):
        """
        Processes revocation infos stored in Zookeeper.
        """
        with self._rev_seg_lock:
            for raw in rev_infos:
                try:
                    rev_info = RevocationInfo.from_raw(raw)
                except SCIONParseError as e:
                    logging.error(
                        "Error processing revocation info from ZK: %s", e)
                    continue
                self.local_rev_cache[rev_info] = rev_info.copy()

    def _issue_revocation(self, if_id):
        """
        Store a RevocationInfo in ZK and send a revocation to all BRs.

        :param if_id: The interface that needs to be revoked.
        :type if_id: int
        """
        # Only the master BS issues revocations.
        if not self.zk.have_lock():
            return
        rev_info = self._get_ht_proof(if_id)
        logging.info("Issuing revocation: %s", rev_info.short_desc())
        # Issue revocation to all BRs.
        info = IFStateInfo.from_values(if_id, False, rev_info)
        pld = IFStatePayload.from_values([info])
        for br in self.topology.border_routers:
            br_addr, br_port = br.int_addrs[0].public[0]
            meta = UDPMetadata.from_values(host=br_addr, port=br_port)
            self.send_meta(pld.copy(), meta, (br_addr, br_port))
        self._process_revocation(rev_info)
        self._send_rev_to_local_ps(rev_info)

    def _send_rev_to_local_ps(self, rev_info):
        """
        Sends the given revocation to its local path server.
        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        """
        if self.zk.have_lock() and self.topology.path_servers:
            try:
                addr, port = self.dns_query_topo(PATH_SERVICE)[0]
            except SCIONServiceLookupError:
                # If there are no local path servers, stop here.
                return
            meta = UDPMetadata.from_values(host=addr, port=port)
            self.send_meta(rev_info.copy(), meta)

    def _handle_scmp_revocation(self, pld, meta):
        rev_info = RevocationInfo.from_raw(pld.info.rev_info)
        logging.debug("Received revocation via SCMP: %s (from %s)",
                      rev_info.short_desc(), meta)
        self._process_revocation(rev_info)

    def _handle_revocation(self, rev_info, meta):
        logging.debug("Received revocation via TCP/UDP: %s (from %s)",
                      rev_info.short_desc(), meta)
        if not self._validate_revocation(rev_info):
            return
        self._process_revocation(rev_info)

    def handle_rev_objs(self):
        with self._rev_seg_lock:
            for rev_info in self.local_rev_cache.values():
                self._remove_revoked_pcbs(rev_info)

    def _process_revocation(self, rev_info):
        """
        Removes PCBs containing a revoked interface and sends the revocation
        to the local PS.

        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        """
        assert isinstance(rev_info, RevocationInfo)
        if_id = rev_info.p.ifID
        if not if_id:
            logging.error("Trying to revoke IF with ID 0.")
            return
        with self._rev_seg_lock:
            self.local_rev_cache[rev_info] = rev_info.copy()
        rev_token = rev_info.copy().pack()
        entry_name = "%s:%s" % (hash(rev_token), time.time())
        try:
            self.revobjs_cache.store(entry_name, rev_token)
        except ZkNoConnection as exc:
            logging.error("Unable to store revocation in shared cache "
                          "(no ZK connection): %s" % exc)
        self._remove_revoked_pcbs(rev_info)

    @abstractmethod
    def _remove_revoked_pcbs(self, rev_info):
        """
        Removes the PCBs containing the revoked interface.

        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        """
        raise NotImplementedError

    def _pcb_list_to_remove(self, candidates, rev_info):
        """
        Calculates the list of PCBs to remove.
        Called by _remove_revoked_pcbs.

        :param candidates: Candidate PCBs.
        :type candidates: List
        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        """
        to_remove = []
        processed = set()
        for cand in candidates:
            if cand.id in processed:
                continue
            processed.add(cand.id)
            if not ConnectedHashTree.verify_epoch(rev_info.p.epoch):
                continue

            # If the interface on which we received the PCB is
            # revoked, then the corresponding pcb needs to be removed.
            root_verify = ConnectedHashTree.verify(rev_info,
                                                   self._get_ht_root())
            if (self.addr.isd_as == rev_info.isd_as()
                    and cand.pcb.p.ifID == rev_info.p.ifID and root_verify):
                to_remove.append(cand.id)

            for asm in cand.pcb.iter_asms():
                if self._verify_revocation_for_asm(rev_info, asm, False):
                    to_remove.append(cand.id)

        return to_remove

    def _handle_if_timeouts(self):
        """
        Periodically checks each interface state and issues an if revocation, if
        no keep-alive message was received for IFID_TOUT.
        """
        if_id_last_revoked = defaultdict(int)
        while self.run_flag.is_set():
            start_time = time.time()
            with self.ifid_state_lock:
                for (if_id, if_state) in self.ifid_state.items():
                    cur_epoch = ConnectedHashTree.get_current_epoch()
                    if not if_state.is_expired() or (
                            if_state.is_revoked()
                            and if_id_last_revoked[if_id] == cur_epoch):
                        # Either the interface hasn't timed out, or it's already revoked for this
                        # epoch
                        continue
                    if_id_last_revoked[if_id] = cur_epoch
                    if not if_state.is_revoked():
                        logging.info("IF %d went down.", if_id)
                    self._issue_revocation(if_id)
                    if_state.revoke_if_expired()
            sleep_interval(start_time, self.IF_TIMEOUT_INTERVAL,
                           "Handle IF timeouts")

    def _handle_ifstate_request(self, req, meta):
        # Only master replies to ifstate requests.
        if not self.zk.have_lock():
            return
        assert isinstance(req, IFStateRequest)
        infos = []
        with self.ifid_state_lock:
            if req.p.ifID == IFStateRequest.ALL_INTERFACES:
                ifid_states = self.ifid_state.items()
            elif req.p.ifID in self.ifid_state:
                ifid_states = [(req.p.ifID, self.ifid_state[req.p.ifID])]
            else:
                logging.error(
                    "Received ifstate request from %s for unknown "
                    "interface %s.", meta, req.p.ifID)
                return

            for (ifid, state) in ifid_states:
                # Don't include inactive interfaces in response.
                if state.is_inactive():
                    continue
                info = IFStateInfo.from_values(ifid, state.is_active(),
                                               self._get_ht_proof(ifid))
                infos.append(info)
        if not infos and not self._quiet_startup():
            logging.warning("No IF state info to put in response. Req: %s" %
                            req.short_desc())
            return
        payload = IFStatePayload.from_values(infos)
        self.send_meta(payload, meta, (meta.host, meta.port))
Example #20
0
class SCIONDaemon(SCIONElement):
    """
    The SCION Daemon used for retrieving and combining paths.
    """
    MAX_REQS = 1024
    # Time a path segment is cached at a host (in seconds).
    SEGMENT_TTL = 300
    # Empty Path TTL
    EMPTY_PATH_TTL = SEGMENT_TTL

    def __init__(self, conf_dir, addr, api_addr, run_local_api=False,
                 port=None, spki_cache_dir=GEN_CACHE_PATH, prom_export=None, delete_sock=False):
        """
        Initialize an instance of the class SCIONDaemon.
        """
        super().__init__("sciond", conf_dir, spki_cache_dir=spki_cache_dir,
                         prom_export=prom_export, public=[(addr, port)])
        up_labels = {**self._labels, "type": "up"} if self._labels else None
        down_labels = {**self._labels, "type": "down"} if self._labels else None
        core_labels = {**self._labels, "type": "core"} if self._labels else None
        self.up_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=up_labels)
        self.down_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=down_labels)
        self.core_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL, labels=core_labels)
        self.rev_cache = RevCache()
        # Keep track of requested paths.
        self.requested_paths = ExpiringDict(self.MAX_REQS, PATH_REQ_TOUT)
        self.req_path_lock = threading.Lock()
        self._api_sock = None
        self.daemon_thread = None
        os.makedirs(SCIOND_API_SOCKDIR, exist_ok=True)
        self.api_addr = (api_addr or get_default_sciond_path())
        if delete_sock:
            try:
                os.remove(self.api_addr)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    logging.error("Could not delete socket %s: %s" % (self.api_addr, e))

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.REPLY: self.handle_path_reply,
                PMT.REVOCATION: self.handle_revocation,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }

        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH:
                {SCMPPathClass.REVOKED_IF: self.handle_scmp_revocation},
        }

        if run_local_api:
            self._api_sock = ReliableSocket(bind_unix=(self.api_addr, "sciond"))
            self._socks.add(self._api_sock, self.handle_accept)

    @classmethod
    def start(cls, conf_dir, addr, api_addr=None, run_local_api=False, port=0):
        """
        Initializes and starts a SCIOND instance.
        """
        inst = cls(conf_dir, addr, api_addr, run_local_api, port)
        name = "SCIONDaemon.run %s" % inst.addr.isd_as
        inst.daemon_thread = threading.Thread(
            target=thread_safety_net, args=(inst.run,), name=name, daemon=True)
        inst.daemon_thread.start()
        logging.debug("sciond started with api_addr = %s", inst.api_addr)

    def _get_msg_meta(self, packet, addr, sock):
        if sock != self._udp_sock:
            return packet, SockOnlyMetadata.from_values(sock)  # API socket
        else:
            return super()._get_msg_meta(packet, addr, sock)

    def handle_msg_meta(self, msg, meta):
        """
        Main routine to handle incoming SCION messages.
        """
        if isinstance(meta, SockOnlyMetadata):  # From SCIOND API
            try:
                sciond_msg = SCIONDMsg.from_raw(msg)
            except SCIONParseError as err:
                logging.error(str(err))
                return
            self.api_handle_request(sciond_msg, meta)
            return
        super().handle_msg_meta(msg, meta)

    def handle_path_reply(self, cpld, meta):
        """
        Handle path reply from local path server.
        """
        pmgt = cpld.union
        path_reply = pmgt.union
        assert isinstance(path_reply, PathSegmentReply), type(path_reply)
        recs = path_reply.recs()
        for srev_info in recs.iter_srev_infos():
            self.check_revocation(srev_info, lambda x: self.continue_revocation_processing(
                                  srev_info) if not x else False, meta)

        req = path_reply.req()
        key = req.dst_ia(), req.flags()
        with self.req_path_lock:
            r = self.requested_paths.get(key)
            if r:
                r.notify_reply(path_reply)
            else:
                logging.warning("No outstanding request found for %s", key)
        for type_, pcb in recs.iter_pcbs():
            seg_meta = PathSegMeta(pcb, self.continue_seg_processing,
                                   meta, type_, params=(r,))
            self._process_path_seg(seg_meta, cpld.req_id)

    def continue_revocation_processing(self, srev_info):
        self.rev_cache.add(srev_info)
        self.remove_revoked_segments(srev_info.rev_info())

    def continue_seg_processing(self, seg_meta):
        """
        For every path segment(that can be verified) received from the path
        server this function gets called to continue the processing for the
        segment.
        The segment is added to pathdb and pending requests are checked.
        """
        pcb = seg_meta.seg
        type_ = seg_meta.type
        # Check that segment does not contain a revoked interface.
        if not self.check_revoked_interface(pcb, self.rev_cache):
            return
        map_ = {
            PST.UP: self._handle_up_seg,
            PST.DOWN: self._handle_down_seg,
            PST.CORE: self._handle_core_seg,
        }
        map_[type_](pcb)
        r = seg_meta.params[0]
        if r:
            r.verified_segment()

    def _handle_up_seg(self, pcb):
        if self.addr.isd_as != pcb.last_ia():
            return None
        if self.up_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Up segment added: %s", pcb.short_desc())
            return pcb.first_ia()
        return None

    def _handle_down_seg(self, pcb):
        last_ia = pcb.last_ia()
        if self.addr.isd_as == last_ia:
            return None
        if self.down_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Down segment added: %s", pcb.short_desc())
            return last_ia
        return None

    def _handle_core_seg(self, pcb):
        if self.core_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Core segment added: %s", pcb.short_desc())
            return pcb.first_ia()
        return None

    def api_handle_request(self, msg, meta):
        """
        Handle local API's requests.
        """
        mtype = msg.type()
        if mtype == SMT.PATH_REQUEST:
            threading.Thread(
                target=thread_safety_net,
                args=(self._api_handle_path_request, msg, meta),
                daemon=True).start()
        elif mtype == SMT.REVOCATION:
            self._api_handle_rev_notification(msg, meta)
        elif mtype == SMT.AS_REQUEST:
            self._api_handle_as_request(msg, meta)
        elif mtype == SMT.IF_REQUEST:
            self._api_handle_if_request(msg, meta)
        elif mtype == SMT.SERVICE_REQUEST:
            self._api_handle_service_request(msg, meta)
        elif mtype == SMT.SEGTYPEHOP_REQUEST:
            self._api_handle_seg_type_request(msg, meta)
        else:
            logging.warning(
                "API: type %s not supported.", TypeBase.to_str(mtype))

    def _api_handle_path_request(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDPathRequest), type(request)
        req_id = pld.id

        dst_ia = request.dst_ia()
        src_ia = request.src_ia()
        if not src_ia:
            src_ia = self.addr.isd_as
        thread = threading.current_thread()
        thread.name = "SCIONDaemon API id:%s %s -> %s" % (
            thread.ident, src_ia, dst_ia)
        paths, error = self.get_paths(dst_ia, flush=request.p.flags.refresh)
        if request.p.maxPaths:
            paths = paths[:request.p.maxPaths]

        reply_entries = []
        for path_meta in paths:
            fwd_if = path_meta.fwd_path().get_fwd_if()
            # Set dummy host addr if path is empty.
            haddr, port = None, None
            if fwd_if:
                br = self.ifid2br[fwd_if]
                haddr, port = br.int_addrs.public[0]
            addrs = [haddr] if haddr else []
            first_hop = HostInfo.from_values(addrs, port)
            reply_entry = SCIONDPathReplyEntry.from_values(
                path_meta, first_hop)
            reply_entries.append(reply_entry)
        logging.debug("Replying to api request for %s with %d paths:\n%s",
                      dst_ia, len(paths), "\n".join([p.short_desc() for p in paths]))
        self._send_path_reply(req_id, reply_entries, error, meta)

    def _send_path_reply(self, req_id, reply_entries, error, meta):
        path_reply = SCIONDMsg(SCIONDPathReply.from_values(reply_entries, error), req_id)
        self.send_meta(path_reply.pack(), meta)

    def _api_handle_as_request(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDASInfoRequest), type(request)
        req_ia = request.isd_as()
        if not req_ia or req_ia.is_zero() or req_ia == self.addr.isd_as:
            # Request is for the local AS.
            reply_entry = SCIONDASInfoReplyEntry.from_values(
                self.addr.isd_as, self.is_core_as(), self.topology.mtu)
        else:
            # Request is for a remote AS.
            reply_entry = SCIONDASInfoReplyEntry.from_values(req_ia, self.is_core_as(req_ia))
        as_reply = SCIONDMsg(SCIONDASInfoReply.from_values([reply_entry]), pld.id)
        self.send_meta(as_reply.pack(), meta)

    def _api_handle_if_request(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDIFInfoRequest), type(request)
        all_brs = request.all_brs()
        if_list = []
        if not all_brs:
            if_list = list(request.iter_ids())
        if_entries = []
        for if_id, br in self.ifid2br.items():
            if all_brs or if_id in if_list:
                br_addr, br_port = br.int_addrs.public[0]
                info = HostInfo.from_values([br_addr], br_port)
                reply_entry = SCIONDIFInfoReplyEntry.from_values(if_id, info)
                if_entries.append(reply_entry)
        if_reply = SCIONDMsg(SCIONDIFInfoReply.from_values(if_entries), pld.id)
        self.send_meta(if_reply.pack(), meta)

    def _api_handle_service_request(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDServiceInfoRequest), type(request)
        all_svcs = request.all_services()
        svc_list = []
        if not all_svcs:
            svc_list = list(request.iter_service_types())
        svc_entries = []
        for svc_type in ServiceType.all():
            if all_svcs or svc_type in svc_list:
                lookup_res = self.dns_query_topo(svc_type)
                host_infos = []
                for addr, port in lookup_res:
                    host_infos.append(HostInfo.from_values([addr], port))
                reply_entry = SCIONDServiceInfoReplyEntry.from_values(
                    svc_type, host_infos)
                svc_entries.append(reply_entry)
        svc_reply = SCIONDMsg(SCIONDServiceInfoReply.from_values(svc_entries), pld.id)
        self.send_meta(svc_reply.pack(), meta)

    def _api_handle_rev_notification(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDRevNotification), type(request)
        self.handle_revocation(CtrlPayload(PathMgmt(request.srev_info())), meta, pld)

    def _api_handle_seg_type_request(self, pld, meta):
        request = pld.union
        assert isinstance(request, SCIONDSegTypeHopRequest), type(request)
        segmentType = request.p.type
        db = []
        if segmentType == PST.CORE:
            db = self.core_segments
        elif segmentType == PST.UP:
            db = self.up_segments
        elif segmentType == PST.DOWN:
            db = self.down_segments
        else:
            logging.error("Requesting segment type %s unrecognized.", segmentType)

        seg_entries = []
        for segment in db(full=True):
            if_list = []
            for asm in segment.iter_asms():
                isd_as = asm.isd_as()
                hof = asm.pcbm(0).hof()
                egress = hof.egress_if
                ingress = hof.ingress_if
                if ingress:
                    if_list.append(PathInterface.from_values(isd_as, ingress))
                if egress:
                    if_list.append(PathInterface.from_values(isd_as, egress))
            reply_entry = SCIONDSegTypeHopReplyEntry.from_values(
                if_list, segment.get_timestamp(), segment.get_expiration_time())
            seg_entries.append(reply_entry)
        seg_reply = SCIONDMsg(
            SCIONDSegTypeHopReply.from_values(seg_entries), pld.id)
        self.send_meta(seg_reply.pack(), meta)

    def handle_scmp_revocation(self, pld, meta):
        srev_info = SignedRevInfo.from_raw(pld.info.srev_info)
        self.handle_revocation(CtrlPayload(PathMgmt(srev_info)), meta)

    def handle_revocation(self, cpld, meta, pld=None):
        pmgt = cpld.union
        srev_info = pmgt.union
        rev_info = srev_info.rev_info()
        assert isinstance(rev_info, RevocationInfo), type(rev_info)
        logging.debug("Received revocation: %s from %s", srev_info.short_desc(), meta)
        self.check_revocation(srev_info,
                              lambda e: self.process_revocation(e, srev_info, meta, pld), meta)

    def process_revocation(self, error, srev_info, meta, pld):
        rev_info = srev_info.rev_info()
        status = None
        if error is None:
            status = SCIONDRevReplyStatus.VALID
            self.rev_cache.add(srev_info)
            self.remove_revoked_segments(rev_info)
        else:
            if type(error) == RevInfoValidationError:
                logging.error("Failed to validate RevInfo %s from %s: %s",
                              srev_info.short_desc(), meta, error)
                status = SCIONDRevReplyStatus.INVALID
            if type(error) == RevInfoExpiredError:
                logging.info("Ignoring expired Revinfo, %s from %s", srev_info.short_desc(), meta)
                status = SCIONDRevReplyStatus.STALE
            if type(error) == SignedRevInfoCertFetchError:
                logging.error("Failed to fetch certificate for SignedRevInfo %s from %s: %s",
                              srev_info.short_desc(), meta, error)
                status = SCIONDRevReplyStatus.UNKNOWN
            if type(error) == SignedRevInfoVerificationError:
                logging.error("Failed to verify SRevInfo %s from %s: %s",
                              srev_info.short_desc(), meta, error)
                status = SCIONDRevReplyStatus.SIGFAIL
            if type(error) == SCIONBaseError:
                logging.error("Revocation check failed for %s from %s:\n%s",
                              srev_info.short_desc(), meta, error)
                status = SCIONDRevReplyStatus.UNKNOWN

        if pld:
            rev_reply = SCIONDMsg(SCIONDRevReply.from_values(status), pld.id)
            self.send_meta(rev_reply.pack(), meta)

    def remove_revoked_segments(self, rev_info):
        # Go through all segment databases and remove affected segments.
        removed_up = removed_core = removed_down = 0
        if rev_info.p.linkType == LinkType.CORE:
            removed_core = self._remove_revoked_pcbs(self.core_segments, rev_info)
        elif rev_info.p.linkType in [LinkType.PARENT, LinkType.CHILD]:
            removed_up = self._remove_revoked_pcbs(self.up_segments, rev_info)
            removed_down = self._remove_revoked_pcbs(self.down_segments, rev_info)
        elif rev_info.p.linkType != LinkType.PEER:
            logging.error("Bad RevInfo link type: %s", rev_info.p.linkType)

        logging.info("Removed %d UP- %d CORE- and %d DOWN-Segments." %
                     (removed_up, removed_core, removed_down))

    def _remove_revoked_pcbs(self, db, rev_info):
        """
        Removes all segments from 'db' that have a revoked upstream PCBMarking.

        :param db: The PathSegmentDB.
        :type db: :class:`lib.path_db.PathSegmentDB`
        :param rev_info: The revocation info
        :type rev_info: RevocationInfo

        :returns: The number of deletions.
        :rtype: int
        """

        to_remove = []
        for segment in db(full=True):
            for asm in segment.iter_asms():
                if self._check_revocation_for_asm(rev_info, asm, verify_all=False):
                    logging.debug("Removing segment: %s" % segment.short_desc())
                    to_remove.append(segment.get_hops_hash())
        return db.delete_all(to_remove)

    def _flush_path_dbs(self):
        self.core_segments.flush()
        self.down_segments.flush()
        self.up_segments.flush()

    def get_paths(self, dst_ia, flags=(), flush=False):
        """Return a list of paths."""
        logging.debug("Paths requested for ISDAS=%s, flags=%s, flush=%s",
                      dst_ia, flags, flush)
        if flush:
            logging.info("Flushing PathDBs.")
            self._flush_path_dbs()
        if self.addr.isd_as == dst_ia or (
                self.addr.isd_as.any_as() == dst_ia and
                self.topology.is_core_as):
            # Either the destination is the local AS, or the destination is any
            # core AS in this ISD, and the local AS is in the core
            empty = SCIONPath()
            exp_time = int(time.time()) + self.EMPTY_PATH_TTL
            empty_meta = FwdPathMeta.from_values(empty, [], self.topology.mtu, exp_time)
            return [empty_meta], SCIONDPathReplyError.OK
        paths = self.path_resolution(dst_ia, flags=flags)
        if not paths:
            key = dst_ia, flags
            with self.req_path_lock:
                r = self.requested_paths.get(key)
                if r is None:
                    # No previous outstanding request
                    req = PathSegmentReq.from_values(self.addr.isd_as, dst_ia, flags=flags)
                    r = RequestState(req.copy())
                    self.requested_paths[key] = r
                    self._fetch_segments(req)
            # Wait until event gets set.
            timeout = not r.e.wait(PATH_REQ_TOUT)
            with self.req_path_lock:
                if timeout:
                    r.done()
                if key in self.requested_paths:
                    del self.requested_paths[key]
            if timeout:
                logging.error("Query timed out for %s", dst_ia)
                return [], SCIONDPathReplyError.PS_TIMEOUT
            # Check if we can fulfill the path request.
            paths = self.path_resolution(dst_ia, flags=flags)
            if not paths:
                logging.error("No paths found for %s", dst_ia)
                return [], SCIONDPathReplyError.NO_PATHS
        return paths, SCIONDPathReplyError.OK

    def path_resolution(self, dst_ia, flags=()):
        # dst as == 0 means any core AS in the specified ISD.
        dst_is_core = self.is_core_as(dst_ia) or dst_ia[1] == 0
        sibra = PATH_FLAG_SIBRA in flags
        if self.topology.is_core_as:
            if dst_is_core:
                ret = self._resolve_core_core(dst_ia, sibra=sibra)
            else:
                ret = self._resolve_core_not_core(dst_ia, sibra=sibra)
        elif dst_is_core:
            ret = self._resolve_not_core_core(dst_ia, sibra=sibra)
        elif sibra:
            ret = self._resolve_not_core_not_core_sibra(dst_ia)
        else:
            ret = self._resolve_not_core_not_core_scion(dst_ia)
        if not sibra:
            return ret
        # FIXME(kormat): Strip off PCBs, and just return sibra reservation
        # blocks
        return self._sibra_strip_pcbs(self._strip_nones(ret))

    def _resolve_core_core(self, dst_ia, sibra=False):
        """Resolve path from core to core."""
        res = set()
        for cseg in self.core_segments(last_ia=self.addr.isd_as, sibra=sibra,
                                       **dst_ia.params()):
            res.add((None, cseg, None))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_core_not_core(self, dst_ia, sibra=False):
        """Resolve path from core to non-core."""
        res = set()
        # First check whether there is a direct path.
        for dseg in self.down_segments(
                first_ia=self.addr.isd_as, last_ia=dst_ia, sibra=sibra):
            res.add((None, None, dseg))
        # Check core-down combination.
        for dseg in self.down_segments(last_ia=dst_ia, sibra=sibra):
            dseg_ia = dseg.first_ia()
            if self.addr.isd_as == dseg_ia:
                pass
            for cseg in self.core_segments(
                    first_ia=dseg_ia, last_ia=self.addr.isd_as, sibra=sibra):
                res.add((None, cseg, dseg))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_not_core_core(self, dst_ia, sibra=False):
        """Resolve path from non-core to core."""
        res = set()
        params = dst_ia.params()
        params["sibra"] = sibra
        if dst_ia[0] == self.addr.isd_as[0]:
            # Dst in local ISD. First check whether DST is a (super)-parent.
            for useg in self.up_segments(**params):
                res.add((useg, None, None))
        # Check whether dst is known core AS.
        for cseg in self.core_segments(**params):
            # Check do we have an up-seg that is connected to core_seg.
            for useg in self.up_segments(first_ia=cseg.last_ia(), sibra=sibra):
                res.add((useg, cseg, None))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_not_core_not_core_scion(self, dst_ia):
        """Resolve SCION path from non-core to non-core."""
        up_segs = self.up_segments()
        down_segs = self.down_segments(last_ia=dst_ia)
        core_segs = self._calc_core_segs(dst_ia[0], up_segs, down_segs)
        full_paths = build_shortcut_paths(
            up_segs, down_segs, self.rev_cache)
        tuples = []
        for up_seg in up_segs:
            for down_seg in down_segs:
                tuples.append((up_seg, None, down_seg))
                for core_seg in core_segs:
                    tuples.append((up_seg, core_seg, down_seg))
        full_paths.extend(tuples_to_full_paths(tuples))
        return full_paths

    def _resolve_not_core_not_core_sibra(self, dst_ia):
        """Resolve SIBRA path from non-core to non-core."""
        res = set()
        up_segs = set(self.up_segments(sibra=True))
        down_segs = set(self.down_segments(last_ia=dst_ia, sibra=True))
        for up_seg, down_seg in product(up_segs, down_segs):
            src_core_ia = up_seg.first_ia()
            dst_core_ia = down_seg.first_ia()
            if src_core_ia == dst_core_ia:
                res.add((up_seg, down_seg))
                continue
            for core_seg in self.core_segments(first_ia=dst_core_ia,
                                               last_ia=src_core_ia, sibra=True):
                res.add((up_seg, core_seg, down_seg))
        return res

    def _strip_nones(self, set_):
        """Strip None entries from a set of tuples"""
        res = []
        for tup in set_:
            res.append(tuple(filter(None, tup)))
        return res

    def _sibra_strip_pcbs(self, paths):
        ret = []
        for pcbs in paths:
            resvs = []
            for pcb in pcbs:
                resvs.append(self._sibra_strip_pcb(pcb))
            ret.append(resvs)
        return ret

    def _sibra_strip_pcb(self, pcb):
        assert pcb.is_sibra()
        pcb_ext = pcb.sibra_ext
        resv_info = pcb_ext.info
        resv = ResvBlockSteady.from_values(resv_info, pcb.get_n_hops())
        asms = pcb.iter_asms()
        if pcb_ext.p.up:
            asms = reversed(list(asms))
        iflist = []
        for sof, asm in zip(pcb_ext.iter_sofs(), asms):
            resv.sofs.append(sof)
            iflist.extend(self._sibra_add_ifs(
                asm.isd_as(), sof, resv_info.fwd_dir))
        assert resv.num_hops == len(resv.sofs)
        return pcb_ext.p.id, resv, iflist

    def _sibra_add_ifs(self, isd_as, sof, fwd):
        def _add(ifid):
            if ifid:
                ret.append((isd_as, ifid))
        ret = []
        if fwd:
            _add(sof.ingress)
            _add(sof.egress)
        else:
            _add(sof.egress)
            _add(sof.ingress)
        return ret

    def _wait_for_events(self, events, deadline):
        """
        Wait on a set of events, but only until the specified deadline. Returns
        the number of events that happened while waiting.
        """
        count = 0
        for e in events:
            if e.wait(max(0, deadline - SCIONTime.get_time())):
                count += 1
        return count

    def _fetch_segments(self, req):
        """
        Called to fetch the requested path.
        """
        try:
            addr, port = self.dns_query_topo(ServiceType.PS)[0]
        except SCIONServiceLookupError:
            log_exception("Error querying path service:")
            return
        req_id = mk_ctrl_req_id()
        logging.debug("Sending path request (%s) to [%s]:%s [id: %016x]",
                      req.short_desc(), addr, port, req_id)
        meta = self._build_meta(host=addr, port=port)
        self.send_meta(CtrlPayload(PathMgmt(req), req_id=req_id), meta)

    def _calc_core_segs(self, dst_isd, up_segs, down_segs):
        """
        Calculate all possible core segments joining the provided up and down
        segments. Returns a list of all known segments, and a seperate list of
        the missing AS pairs.
        """
        src_core_ases = set()
        dst_core_ases = set()
        for seg in up_segs:
            src_core_ases.add(seg.first_ia()[1])
        for seg in down_segs:
            dst_core_ases.add(seg.first_ia()[1])
        # Generate all possible AS pairs
        as_pairs = list(product(src_core_ases, dst_core_ases))
        return self._find_core_segs(self.addr.isd_as[0], dst_isd, as_pairs)

    def _find_core_segs(self, src_isd, dst_isd, as_pairs):
        """
        Given a set of AS pairs across 2 ISDs, return the core segments
        connecting those pairs
        """
        core_segs = []
        for src_core_as, dst_core_as in as_pairs:
            src_ia = ISD_AS.from_values(src_isd, src_core_as)
            dst_ia = ISD_AS.from_values(dst_isd, dst_core_as)
            if src_ia == dst_ia:
                continue
            seg = self.core_segments(first_ia=dst_ia, last_ia=src_ia)
            if seg:
                core_segs.extend(seg)
        return core_segs

    def run(self):
        """
        Run an instance of the SCION daemon.
        """
        threading.Thread(
            target=thread_safety_net, args=(self._check_trc_cert_reqs,),
            name="Elem.check_trc_cert_reqs", daemon=True).start()
        super().run()
Example #21
0
class SCIONDaemon(SCIONElement):
    """
    The SCION Daemon used for retrieving and combining paths.
    """
    # Max time for a path lookup to succeed/fail.
    PATH_REQ_TOUT = 2
    MAX_REQS = 1024
    # Time a path segment is cached at a host (in seconds).
    SEGMENT_TTL = 300

    def __init__(self,
                 conf_dir,
                 addr,
                 api_addr,
                 run_local_api=False,
                 port=None):
        """
        Initialize an instance of the class SCIONDaemon.
        """
        super().__init__("sciond", conf_dir, host_addr=addr, port=port)
        # TODO replace by pathstore instance
        self.up_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL)
        self.down_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL)
        self.core_segments = PathSegmentDB(segment_ttl=self.SEGMENT_TTL)
        self.peer_revs = RevCache()
        # Keep track of requested paths.
        self.requested_paths = ExpiringDict(self.MAX_REQS, self.PATH_REQ_TOUT)
        self.req_path_lock = threading.Lock()
        self._api_sock = None
        self.daemon_thread = None
        os.makedirs(SCIOND_API_SOCKDIR, exist_ok=True)
        self.api_addr = (api_addr or os.path.join(
            SCIOND_API_SOCKDIR, "%s.sock" % self.addr.isd_as))

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.REPLY: self.handle_path_reply,
                PMT.REVOCATION: self.handle_revocation,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }

        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self.handle_scmp_revocation
            },
        }

        if run_local_api:
            self._api_sock = ReliableSocket(bind=(self.api_addr, "sciond"))
            self._socks.add(self._api_sock, self.handle_accept)

    @classmethod
    def start(cls, conf_dir, addr, api_addr=None, run_local_api=False, port=0):
        """
        Initializes, starts, and returns a SCIONDaemon object.

        Example of usage:
        sd = SCIONDaemon.start(conf_dir, addr)
        paths = sd.get_paths(isd_as)
        """
        inst = cls(conf_dir, addr, api_addr, run_local_api, port)
        name = "SCIONDaemon.run %s" % inst.addr.isd_as
        inst.daemon_thread = threading.Thread(target=thread_safety_net,
                                              args=(inst.run, ),
                                              name=name,
                                              daemon=True)
        inst.daemon_thread.start()
        logging.debug("sciond started with api_addr = %s", inst.api_addr)
        return inst

    def _get_msg_meta(self, packet, addr, sock):
        if sock != self._udp_sock:
            return packet, SockOnlyMetadata.from_values(sock)  # API socket
        else:
            return super()._get_msg_meta(packet, addr, sock)

    def handle_msg_meta(self, msg, meta):
        """
        Main routine to handle incoming SCION messages.
        """
        if isinstance(meta, SockOnlyMetadata):  # From SCIOND API
            try:
                sciond_msg = parse_sciond_msg(msg)
            except SCIONParseError as err:
                logging.error(str(err))
                return
            self.api_handle_request(sciond_msg, meta)
            return
        super().handle_msg_meta(msg, meta)

    def handle_path_reply(self, path_reply, meta):
        """
        Handle path reply from local path server.
        """
        for rev_info in path_reply.iter_rev_infos():
            self.peer_revs.add(rev_info)

        for type_, pcb in path_reply.iter_pcbs():
            seg_meta = PathSegMeta(pcb, self.continue_seg_processing, meta,
                                   type_)
            self.process_path_seg(seg_meta)

    def continue_seg_processing(self, seg_meta):
        """
        For every path segment(that can be verified) received from the path
        server this function gets called to continue the processing for the
        segment.
        The segment is added to pathdb and pending requests are checked.
        """
        pcb = seg_meta.seg
        type_ = seg_meta.type
        map_ = {
            PST.UP: self._handle_up_seg,
            PST.DOWN: self._handle_down_seg,
            PST.CORE: self._handle_core_seg,
        }
        ret = map_[type_](pcb)
        if not ret:
            return
        with self.req_path_lock:
            # .items() makes a copy on an expiring dict, so deleting entries is safe.
            for key, e in self.requested_paths.items():
                if self.path_resolution(*key):
                    e.set()
                    del self.requested_paths[key]

    def _handle_up_seg(self, pcb):
        if self.addr.isd_as != pcb.last_ia():
            return None
        if self.up_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Up segment added: %s", pcb.short_desc())
            return pcb.first_ia()
        return None

    def _handle_down_seg(self, pcb):
        last_ia = pcb.last_ia()
        if self.addr.isd_as == last_ia:
            return None
        if self.down_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Down segment added: %s", pcb.short_desc())
            return last_ia
        return None

    def _handle_core_seg(self, pcb):
        if self.core_segments.update(pcb) == DBResult.ENTRY_ADDED:
            logging.debug("Core segment added: %s", pcb.short_desc())
            return pcb.first_ia()
        return None

    def api_handle_request(self, msg, meta):
        """
        Handle local API's requests.
        """
        if msg.MSG_TYPE == SMT.PATH_REQUEST:
            threading.Thread(target=thread_safety_net,
                             args=(self._api_handle_path_request, msg, meta),
                             daemon=True).start()
        elif msg.MSG_TYPE == SMT.REVOCATION:
            self.handle_revocation(msg.rev_info(), meta)
        elif msg.MSG_TYPE == SMT.AS_REQUEST:
            self._api_handle_as_request(msg, meta)
        elif msg.MSG_TYPE == SMT.IF_REQUEST:
            self._api_handle_if_request(msg, meta)
        elif msg.MSG_TYPE == SMT.SERVICE_REQUEST:
            self._api_handle_service_request(msg, meta)
        else:
            logging.warning("API: type %s not supported.",
                            TypeBase.to_str(msg.MSG_TYPE))

    def _api_handle_path_request(self, request, meta):
        req_id = request.id
        if request.p.flags.sibra:
            logging.warning(
                "Requesting SIBRA paths over SCIOND API not supported yet.")
            self._send_path_reply(req_id, [], SCIONDPathReplyError.INTERNAL,
                                  meta)
            return

        dst_ia = request.dst_ia()
        src_ia = request.src_ia()
        if not src_ia:
            src_ia = self.addr.isd_as
        thread = threading.current_thread()
        thread.name = "SCIONDaemon API id:%s %s -> %s" % (thread.ident, src_ia,
                                                          dst_ia)
        paths, error = self.get_paths(dst_ia, flush=request.p.flags.flush)
        if request.p.maxPaths:
            paths = paths[:request.p.maxPaths]
        logging.debug("Replying to api request for %s with %d paths", dst_ia,
                      len(paths))
        reply_entries = []
        for path_meta in paths:
            fwd_if = path_meta.fwd_path().get_fwd_if()
            # Set dummy host addr if path is empty.
            haddr, port = None, None
            if fwd_if:
                br = self.ifid2br[fwd_if]
                haddr, port = br.addr, br.port
            addrs = [haddr] if haddr else []
            first_hop = HostInfo.from_values(addrs, port)
            reply_entry = SCIONDPathReplyEntry.from_values(
                path_meta, first_hop)
            reply_entries.append(reply_entry)
        self._send_path_reply(req_id, reply_entries, error, meta)

    def _send_path_reply(self, req_id, reply_entries, error, meta):
        path_reply = SCIONDPathReply.from_values(req_id, reply_entries, error)
        self.send_meta(path_reply.pack_full(), meta)

    def _api_handle_as_request(self, request, meta):
        remote_as = request.isd_as()
        if remote_as:
            reply_entry = SCIONDASInfoReplyEntry.from_values(
                remote_as, self.is_core_as(remote_as))
        else:
            reply_entry = SCIONDASInfoReplyEntry.from_values(
                self.addr.isd_as, self.is_core_as(), self.topology.mtu)
        as_reply = SCIONDASInfoReply.from_values(request.id, [reply_entry])
        self.send_meta(as_reply.pack_full(), meta)

    def _api_handle_if_request(self, request, meta):
        all_brs = request.all_brs()
        if_list = []
        if not all_brs:
            if_list = list(request.iter_ids())
        if_entries = []
        for if_id, br in self.ifid2br.items():
            if all_brs or if_id in if_list:
                info = HostInfo.from_values([br.addr], br.port)
                reply_entry = SCIONDIFInfoReplyEntry.from_values(if_id, info)
                if_entries.append(reply_entry)
        if_reply = SCIONDIFInfoReply.from_values(request.id, if_entries)
        self.send_meta(if_reply.pack_full(), meta)

    def _api_handle_service_request(self, request, meta):
        all_svcs = request.all_services()
        svc_list = []
        if not all_svcs:
            svc_list = list(request.iter_service_types())
        svc_entries = []
        for svc_type in ServiceType.all():
            if all_svcs or svc_type in svc_list:
                lookup_res = self.dns_query_topo(svc_type)
                host_infos = []
                for addr, port in lookup_res:
                    host_infos.append(HostInfo.from_values([addr], port))
                reply_entry = SCIONDServiceInfoReplyEntry.from_values(
                    svc_type, host_infos)
                svc_entries.append(reply_entry)
        svc_reply = SCIONDServiceInfoReply.from_values(request.id, svc_entries)
        self.send_meta(svc_reply.pack_full(), meta)

    def handle_scmp_revocation(self, pld, meta):
        rev_info = RevocationInfo.from_raw(pld.info.rev_info)
        self.handle_revocation(rev_info, meta)

    def handle_revocation(self, rev_info, meta):
        assert isinstance(rev_info, RevocationInfo)
        if not self._validate_revocation(rev_info):
            return
        # Go through all segment databases and remove affected segments.
        removed_up = self._remove_revoked_pcbs(self.up_segments, rev_info)
        removed_core = self._remove_revoked_pcbs(self.core_segments, rev_info)
        removed_down = self._remove_revoked_pcbs(self.down_segments, rev_info)
        logging.info("Removed %d UP- %d CORE- and %d DOWN-Segments." %
                     (removed_up, removed_core, removed_down))

    def _remove_revoked_pcbs(self, db, rev_info):
        """
        Removes all segments from 'db' that contain an IF token for which
        rev_token is a preimage (within 20 calls).

        :param db: The PathSegmentDB.
        :type db: :class:`lib.path_db.PathSegmentDB`
        :param rev_info: The revocation info
        :type rev_info: RevocationInfo

        :returns: The number of deletions.
        :rtype: int
        """

        if not ConnectedHashTree.verify_epoch(rev_info.p.epoch):
            logging.debug(
                "Failed to verify epoch: rev_info epoch %d,current epoch %d." %
                (rev_info.p.epoch, ConnectedHashTree.get_current_epoch()))
            return 0

        to_remove = []
        for segment in db(full=True):
            for asm in segment.iter_asms():
                if self._verify_revocation_for_asm(rev_info, asm):
                    logging.debug("Removing segment: %s" %
                                  segment.short_desc())
                    to_remove.append(segment.get_hops_hash())
        return db.delete_all(to_remove)

    def _flush_path_dbs(self):
        self.core_segments.flush()
        self.down_segments.flush()
        self.up_segments.flush()

    def get_paths(self, dst_ia, flags=(), flush=False):
        """Return a list of paths."""
        logging.debug("Paths requested for ISDAS=%s, flags=%s, flush=%s",
                      dst_ia, flags, flush)
        if flush:
            logging.info("Flushing PathDBs.")
            self._flush_path_dbs()
        if self.addr.isd_as == dst_ia or (self.addr.isd_as.any_as() == dst_ia
                                          and self.topology.is_core_as):
            # Either the destination is the local AS, or the destination is any
            # core AS in this ISD, and the local AS is in the core
            empty = SCIONPath()
            empty_meta = FwdPathMeta.from_values(empty, [], self.topology.mtu)
            return [empty_meta], SCIONDPathReplyError.OK
        paths = self.path_resolution(dst_ia, flags=flags)
        if not paths:
            key = dst_ia, flags
            with self.req_path_lock:
                if key not in self.requested_paths:
                    # No previous outstanding request
                    self.requested_paths[key] = threading.Event()
                    self._fetch_segments(key)
                e = self.requested_paths[key]
            if not e.wait(self.PATH_REQ_TOUT):
                logging.error("Query timed out for %s", dst_ia)
                return [], SCIONDPathReplyError.PS_TIMEOUT
            paths = self.path_resolution(dst_ia, flags=flags)
        error_code = (SCIONDPathReplyError.OK
                      if paths else SCIONDPathReplyError.NO_PATHS)
        return paths, error_code

    def path_resolution(self, dst_ia, flags=()):
        # dst as == 0 means any core AS in the specified ISD.
        dst_is_core = self.is_core_as(dst_ia) or dst_ia[1] == 0
        sibra = PATH_FLAG_SIBRA in flags
        if self.topology.is_core_as:
            if dst_is_core:
                ret = self._resolve_core_core(dst_ia, sibra=sibra)
            else:
                ret = self._resolve_core_not_core(dst_ia, sibra=sibra)
        elif dst_is_core:
            ret = self._resolve_not_core_core(dst_ia, sibra=sibra)
        elif sibra:
            ret = self._resolve_not_core_not_core_sibra(dst_ia)
        else:
            ret = self._resolve_not_core_not_core_scion(dst_ia)
        if not sibra:
            return ret
        # FIXME(kormat): Strip off PCBs, and just return sibra reservation
        # blocks
        return self._sibra_strip_pcbs(self._strip_nones(ret))

    def _resolve_core_core(self, dst_ia, sibra=False):
        """Resolve path from core to core."""
        res = set()
        for cseg in self.core_segments(last_ia=self.addr.isd_as,
                                       sibra=sibra,
                                       **dst_ia.params()):
            res.add((None, cseg, None))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_core_not_core(self, dst_ia, sibra=False):
        """Resolve path from core to non-core."""
        res = set()
        # First check whether there is a direct path.
        for dseg in self.down_segments(first_ia=self.addr.isd_as,
                                       last_ia=dst_ia,
                                       sibra=sibra):
            res.add((None, None, dseg))
        # Check core-down combination.
        for dseg in self.down_segments(last_ia=dst_ia, sibra=sibra):
            dseg_ia = dseg.first_ia()
            if self.addr.isd_as == dseg_ia:
                pass
            for cseg in self.core_segments(first_ia=dseg_ia,
                                           last_ia=self.addr.isd_as,
                                           sibra=sibra):
                res.add((None, cseg, dseg))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_not_core_core(self, dst_ia, sibra=False):
        """Resolve path from non-core to core."""
        res = set()
        params = dst_ia.params()
        params["sibra"] = sibra
        if dst_ia[0] == self.addr.isd_as[0]:
            # Dst in local ISD. First check whether DST is a (super)-parent.
            for useg in self.up_segments(**params):
                res.add((useg, None, None))
        # Check whether dst is known core AS.
        for cseg in self.core_segments(**params):
            # Check do we have an up-seg that is connected to core_seg.
            for useg in self.up_segments(first_ia=cseg.last_ia(), sibra=sibra):
                res.add((useg, cseg, None))
        if sibra:
            return res
        return tuples_to_full_paths(res)

    def _resolve_not_core_not_core_scion(self, dst_ia):
        """Resolve SCION path from non-core to non-core."""
        up_segs = self.up_segments()
        down_segs = self.down_segments(last_ia=dst_ia)
        core_segs = self._calc_core_segs(dst_ia[0], up_segs, down_segs)
        full_paths = build_shortcut_paths(up_segs, down_segs, self.peer_revs)
        tuples = []
        for up_seg in up_segs:
            for down_seg in down_segs:
                tuples.append((up_seg, None, down_seg))
                for core_seg in core_segs:
                    tuples.append((up_seg, core_seg, down_seg))
        full_paths.extend(tuples_to_full_paths(tuples))
        return full_paths

    def _resolve_not_core_not_core_sibra(self, dst_ia):
        """Resolve SIBRA path from non-core to non-core."""
        res = set()
        up_segs = set(self.up_segments(sibra=True))
        down_segs = set(self.down_segments(last_ia=dst_ia, sibra=True))
        for up_seg, down_seg in product(up_segs, down_segs):
            src_core_ia = up_seg.first_ia()
            dst_core_ia = down_seg.first_ia()
            if src_core_ia == dst_core_ia:
                res.add((up_seg, down_seg))
                continue
            for core_seg in self.core_segments(first_ia=dst_core_ia,
                                               last_ia=src_core_ia,
                                               sibra=True):
                res.add((up_seg, core_seg, down_seg))
        return res

    def _strip_nones(self, set_):
        """Strip None entries from a set of tuples"""
        res = []
        for tup in set_:
            res.append(tuple(filter(None, tup)))
        return res

    def _sibra_strip_pcbs(self, paths):
        ret = []
        for pcbs in paths:
            resvs = []
            for pcb in pcbs:
                resvs.append(self._sibra_strip_pcb(pcb))
            ret.append(resvs)
        return ret

    def _sibra_strip_pcb(self, pcb):
        assert pcb.is_sibra()
        pcb_ext = pcb.sibra_ext
        resv_info = pcb_ext.info
        resv = ResvBlockSteady.from_values(resv_info, pcb.get_n_hops())
        asms = pcb.iter_asms()
        if pcb_ext.p.up:
            asms = reversed(list(asms))
        iflist = []
        for sof, asm in zip(pcb_ext.iter_sofs(), asms):
            resv.sofs.append(sof)
            iflist.extend(
                self._sibra_add_ifs(asm.isd_as(), sof, resv_info.fwd_dir))
        assert resv.num_hops == len(resv.sofs)
        return pcb_ext.p.id, resv, iflist

    def _sibra_add_ifs(self, isd_as, sof, fwd):
        def _add(ifid):
            if ifid:
                ret.append((isd_as, ifid))

        ret = []
        if fwd:
            _add(sof.ingress)
            _add(sof.egress)
        else:
            _add(sof.egress)
            _add(sof.ingress)
        return ret

    def _wait_for_events(self, events, deadline):
        """
        Wait on a set of events, but only until the specified deadline. Returns
        the number of events that happened while waiting.
        """
        count = 0
        for e in events:
            if e.wait(max(0, deadline - SCIONTime.get_time())):
                count += 1
        return count

    def _fetch_segments(self, key):
        """
        Called to fetch the requested path.
        """
        dst_ia, flags = key
        try:
            addr, port = self.dns_query_topo(PATH_SERVICE)[0]
        except SCIONServiceLookupError:
            log_exception("Error querying path service:")
            return
        req = PathSegmentReq.from_values(self.addr.isd_as, dst_ia, flags=flags)
        logging.debug("Sending path request: %s", req.short_desc())
        meta = self.DefaultMeta.from_values(host=addr, port=port)
        self.send_meta(req, meta)

    def _calc_core_segs(self, dst_isd, up_segs, down_segs):
        """
        Calculate all possible core segments joining the provided up and down
        segments. Returns a list of all known segments, and a seperate list of
        the missing AS pairs.
        """
        src_core_ases = set()
        dst_core_ases = set()
        for seg in up_segs:
            src_core_ases.add(seg.first_ia()[1])
        for seg in down_segs:
            dst_core_ases.add(seg.first_ia()[1])
        # Generate all possible AS pairs
        as_pairs = list(product(src_core_ases, dst_core_ases))
        return self._find_core_segs(self.addr.isd_as[0], dst_isd, as_pairs)

    def _find_core_segs(self, src_isd, dst_isd, as_pairs):
        """
        Given a set of AS pairs across 2 ISDs, return the core segments
        connecting those pairs
        """
        core_segs = []
        for src_core_as, dst_core_as in as_pairs:
            src_ia = ISD_AS.from_values(src_isd, src_core_as)
            dst_ia = ISD_AS.from_values(dst_isd, dst_core_as)
            if src_ia == dst_ia:
                continue
            seg = self.core_segments(first_ia=dst_ia, last_ia=src_ia)
            if seg:
                core_segs.extend(seg)
        return core_segs
Example #22
0
class BeaconServer(SCIONElement, metaclass=ABCMeta):
    """
    The SCION PathConstructionBeacon Server.

    Attributes:
        if2rev_tokens: Contains the currently used revocation token
            hash-chain for each interface.
    """
    SERVICE_TYPE = BEACON_SERVICE
    # Amount of time units a HOF is valid (time unit is EXP_TIME_UNIT).
    HOF_EXP_TIME = 63
    # Timeout for TRC or Certificate requests.
    REQUESTS_TIMEOUT = 10
    # ZK path for incoming PCBs
    ZK_PCB_CACHE_PATH = "pcb_cache"
    # ZK path for revocations.
    ZK_REVOCATIONS_PATH = "rev_cache"
    # Time revocation objects are cached in memory (in seconds).
    ZK_REV_OBJ_MAX_AGE = 60 * 60
    # Interval to checked for timed out interfaces.
    IF_TIMEOUT_INTERVAL = 1
    # Number of tokens the BS checks when receiving a revocation.
    N_TOKENS_CHECK = 20

    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        # TODO: add 2 policies
        self.path_policy = PathPolicy.from_file(
            os.path.join(conf_dir, PATH_POLICY_FILE))
        self.unverified_beacons = deque()
        self.trc_requests = {}
        self.trcs = {}
        sig_key_file = get_sig_key_file_path(self.conf_dir)
        self.signing_key = base64.b64decode(read_file(sig_key_file))
        self.of_gen_key = PBKDF2(self.config.master_as_key, b"Derive OF Key")
        logging.info(self.config.__dict__)
        self.if2rev_tokens = {}
        self._if_rev_token_lock = threading.Lock()
        self.revs_to_downstream = ExpiringDict(max_len=1000,
                                               max_age_seconds=60)

        self.ifid_state = {}
        for ifid in self.ifid2er:
            self.ifid_state[ifid] = InterfaceState()

        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PCB: {
                PCBType.SEGMENT: self.handle_pcb
            },
            PayloadClass.IFID: {
                IFIDType.PAYLOAD: self.handle_ifid_packet
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_rep,
                CertMgmtType.TRC_REPLY: self.process_trc_rep,
            },
            PayloadClass.PATH: {
                PMT.IFSTATE_REQ: self._handle_ifstate_request
            },
        }

        # Add more IPs here if we support dual-stack
        name_addrs = "\0".join(
            [self.id, str(SCION_UDP_PORT),
             str(self.addr.host)])
        self.zk = Zookeeper(self.addr.isd_as, BEACON_SERVICE, name_addrs,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.incoming_pcbs = deque()
        self.pcb_cache = ZkSharedCache(self.zk, self.ZK_PCB_CACHE_PATH,
                                       self.process_pcbs)
        self.revobjs_cache = ZkSharedCache(self.zk, self.ZK_REVOCATIONS_PATH,
                                           self.process_rev_objects)

    def _init_hash_chain(self, if_id):
        """
        Setups a hash chain for interface 'if_id'.
        """
        if if_id in self.if2rev_tokens:
            return
        seed = self.config.master_as_key + bytes([if_id])
        start_ele = SHA256.new(seed).digest()
        chain = HashChain(start_ele)
        self.if2rev_tokens[if_id] = chain
        return chain

    def _get_if_hash_chain(self, if_id):
        """
        Returns the hash chain corresponding to interface if_id.
        """
        if not if_id:
            return None
        elif if_id not in self.if2rev_tokens:
            return self._init_hash_chain(if_id)

        return self.if2rev_tokens[if_id]

    def _get_if_rev_token(self, if_id):
        """
        Returns the revocation token for a given interface.

        :param if_id: interface identifier.
        :type if_id: int
        """
        with self._if_rev_token_lock:
            ret = None
            if if_id == 0:
                ret = bytes(32)
            else:
                chain = self._get_if_hash_chain(if_id)
                if chain:
                    ret = chain.current_element()
            return ret

    def propagate_downstream_pcb(self, pcb):
        """
        Propagates the beacon to all children.

        :param pcb: path segment.
        :type pcb: PathSegment
        """
        for r in self.topology.child_edge_routers:
            beacon = self._mk_prop_beacon(pcb.copy(), r.interface.isd_as,
                                          r.interface.if_id)
            self.send(beacon, r.addr)
            logging.info("Downstream PCB propagated!")

    def _mk_prop_beacon(self, pcb, dst_ia, egress_if):
        ts = pcb.get_timestamp()
        asm = self._create_asm(pcb.p.ifID, egress_if, ts, pcb.last_hof())
        pcb.add_asm(asm)
        pcb.sign(self.signing_key)
        return self._build_packet(SVCType.BS, dst_ia=dst_ia, payload=pcb)

    def _mk_if_info(self, if_id):
        """
        Small helper method to make it easier to deal with ingress/egress
        interface being 0 while building ASMarkings.
        """
        d = {"remote_ia": ISD_AS.from_values(0, 0), "remote_if": 0, "mtu": 0}
        if not if_id:
            return d
        er = self.ifid2er[if_id]
        d["remote_ia"] = er.interface.isd_as
        d["remote_if"] = er.interface.if_id
        d["mtu"] = er.interface.mtu
        return d

    @abstractmethod
    def handle_pcbs_propagation(self):
        """
        Main loop to propagate received beacons.
        """
        raise NotImplementedError

    def handle_pcb(self, pkt):
        """Receives beacon and stores it for processing."""
        pcb = pkt.get_payload()
        if not self.path_policy.check_filters(pcb):
            return
        self.incoming_pcbs.append(pcb)
        entry_name = "%s-%s" % (pcb.get_hops_hash(hex=True), time.time())
        try:
            self.pcb_cache.store(entry_name, pcb.copy().pack())
        except ZkNoConnection:
            logging.error("Unable to store PCB in shared cache: "
                          "no connection to ZK")

    def handle_ext(self, pcb):
        """
        Handle beacon extensions.
        """
        # Handle ASMarking extensions:
        for asm in pcb.iter_asms():
            for rev_info in asm.p.exts.revInfos:
                self.rev_ext_handler(rev_info, asm.isd_as())
        # Handle PCB extensions:
        if pcb.is_sibra():
            logging.debug("%s", pcb.sibra_ext)

    def rev_ext_handler(self, rev_info, isd_as):
        logging.info("REV %s: %s" % (isd_as, rev_info))
        # Trigger the removal of PCBs which contain the revoked interface
        self._remove_revoked_pcbs(rev_info=rev_info, if_id=None)
        # Inform the local PS
        self._send_rev_to_local_ps(rev_info=rev_info)

    @abstractmethod
    def process_pcbs(self, pcbs, raw=True):
        """
        Processes new beacons and appends them to beacon list.
        """
        raise NotImplementedError

    def process_pcb_queue(self):
        pcbs = []
        while self.incoming_pcbs:
            pcbs.append(self.incoming_pcbs.popleft())
        self.process_pcbs(pcbs, raw=False)
        logging.debug("Processed %d pcbs from incoming queue", len(pcbs))

    @abstractmethod
    def register_segments(self):
        """
        Registers paths according to the received beacons.
        """
        raise NotImplementedError

    def _create_asm(self, in_if, out_if, ts, prev_hof):
        pcbms = list(self._create_pcbms(in_if, out_if, ts, prev_hof))
        exts = self._create_asm_exts()
        chain = self._get_my_cert()
        _, cert_ver = chain.get_leaf_isd_as_ver()
        return ASMarking.from_values(self.addr.isd_as,
                                     self._get_my_trc().version, cert_ver,
                                     pcbms, self._get_if_rev_token(out_if),
                                     self.topology.mtu, chain, **exts)

    def _create_pcbms(self, in_if, out_if, ts, prev_hof):
        pcbm = self._create_pcbm(in_if, out_if, ts, prev_hof)
        yield pcbm
        for er in sorted(self.topology.peer_edge_routers):
            in_if = er.interface.if_id
            if (not self.ifid_state[in_if].is_active()
                    and not self._quiet_startup()):
                logging.warning('Peer ifid:%d inactive (not added).', in_if)
                continue
            yield self._create_pcbm(in_if, out_if, ts, pcbm.hof(), xover=True)

    def _create_pcbm(self, in_if, out_if, ts, prev_hof, xover=False):
        hof = HopOpaqueField.from_values(self.HOF_EXP_TIME,
                                         in_if,
                                         out_if,
                                         xover=xover)
        hof.set_mac(self.of_gen_key, ts, prev_hof)
        in_info = self._mk_if_info(in_if)
        out_info = self._mk_if_info(out_if)
        return PCBMarking.from_values(in_info["remote_ia"],
                                      in_info["remote_if"], in_info["mtu"],
                                      out_info["remote_ia"],
                                      out_info["remote_if"], hof,
                                      self._get_if_rev_token(in_if))

    def _create_asm_exts(self):
        return {"rev_infos": list(self.revs_to_downstream.items())}

    def _terminate_pcb(self, pcb):
        """
        Copies a PCB, terminates it and adds the segment ID.

        Terminating a PCB means adding a opaque field with the egress IF set
        to 0, i.e., there is no AS to forward a packet containing this path
        segment to.
        """
        pcb = pcb.copy()
        asm = self._create_asm(pcb.p.ifID, 0, pcb.get_timestamp(),
                               pcb.last_hof())
        pcb.add_asm(asm)
        return pcb

    def handle_ifid_packet(self, pkt):
        """
        Update the interface state for the corresponding interface.

        :param ipkt: The IFIDPayload.
        :type ipkt: IFIDPayload
        """
        payload = pkt.get_payload()
        ifid = payload.p.relayIF
        if ifid not in self.ifid_state:
            raise SCIONKeyError("Invalid IF %d in IFIDPayload" % ifid)
        er = self.ifid2er[ifid]
        er.interface.to_if_id = payload.p.origIF
        prev_state = self.ifid_state[ifid].update()
        if prev_state == InterfaceState.INACTIVE:
            logging.info("IF %d activated", ifid)
        elif prev_state in [InterfaceState.TIMED_OUT, InterfaceState.REVOKED]:
            logging.info("IF %d came back up.", ifid)
        if not prev_state == InterfaceState.ACTIVE:
            if self.zk.have_lock():
                # Inform ERs about the interface coming up.
                chain = self._get_if_hash_chain(ifid)
                if chain is None:
                    return
                state_info = IFStateInfo.from_values(ifid, True,
                                                     chain.current_element())
                pld = IFStatePayload.from_values([state_info])
                mgmt_packet = self._build_packet()
                for er in self.topology.get_all_edge_routers():
                    if er.interface.if_id != ifid:
                        mgmt_packet.addrs.dst.host = er.addr
                        mgmt_packet.set_payload(pld.copy())
                        self.send(mgmt_packet, er.addr)

    def run(self):
        """
        Run an instance of the Beacon Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="BS.worker",
                         daemon=True).start()
        # https://github.com/netsec-ethz/scion/issues/308:
        threading.Thread(target=thread_safety_net,
                         args=(self._handle_if_timeouts, ),
                         name="BS._handle_if_timeouts",
                         daemon=True).start()
        super().run()

    def worker(self):
        """
        Worker thread that takes care of reading shared PCBs from ZK, and
        propagating PCBS/registering paths when master.
        """
        last_propagation = last_registration = 0
        worker_cycle = 1.0
        was_master = False
        start = time.time()
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "BS.worker cycle",
                           self._quiet_startup())
            start = time.time()
            try:
                self.process_pcb_queue()
                self.handle_unverified_beacons()
                self.zk.wait_connected()
                self.pcb_cache.process()
                self.revobjs_cache.process()
                if not self.zk.get_lock(lock_timeout=0, conn_timeout=0):
                    was_master = False
                    continue
                if not was_master:
                    self._became_master()
                    was_master = True
                self.pcb_cache.expire(self.config.propagation_time * 10)
                self.revobjs_cache.expire(self.ZK_REV_OBJ_MAX_AGE * 24)
            except ZkNoConnection:
                continue
            now = time.time()
            if now - last_propagation >= self.config.propagation_time:
                self.handle_pcbs_propagation()
                last_propagation = now
            if (self.config.registers_paths and
                    now - last_registration >= self.config.registration_time):
                try:
                    self.register_segments()
                except SCIONKeyError as e:
                    logging.error("Register_segments: %s", e)
                    pass
                last_registration = now

    def _became_master(self):
        """
        Called when a BS becomes the new master. Resets some state that will be
        rebuilt over time.
        """
        # Reset all timed-out and revoked interfaces to inactive.
        for (_, ifstate) in self.ifid_state.items():
            if not ifstate.is_active():
                ifstate.reset()

    def _try_to_verify_beacon(self, pcb, quiet=False):
        """
        Try to verify a beacon.

        :param pcb: path segment to verify.
        :type pcb: PathSegment
        """
        assert isinstance(pcb, PathSegment)
        asm = pcb.asm(-1)
        if self._check_trc(asm.isd_as(), asm.p.trcVer):
            if self._verify_beacon(pcb):
                self._handle_verified_beacon(pcb)
            else:
                logging.warning("Invalid beacon. %s", pcb)
        else:
            if not quiet:
                logging.warning("Certificate(s) or TRC missing for pcb: %s",
                                pcb.short_desc())
            self.unverified_beacons.append(pcb)

    @abstractmethod
    def _check_trc(self, isd_as, trc_ver):
        """
        Return True or False whether the necessary Certificate and TRC files are
        found.

        :param ISD_AS isd_is: ISD-AS identifier.
        :param int trc_ver: TRC file version.
        """
        raise NotImplementedError

    def _get_my_trc(self):
        return self.trust_store.get_trc(self.addr.isd_as[0])

    def _get_my_cert(self):
        return self.trust_store.get_cert(self.addr.isd_as)

    def _get_trc(self, isd_as, trc_ver):
        """
        Get TRC from local storage or memory.

        :param ISD_AS isd_as: ISD-AS identifier.
        :param int trc_ver: TRC file version.
        """
        trc = self.trust_store.get_trc(isd_as[0], trc_ver)
        if not trc:
            # Requesting TRC file from cert server
            trc_tuple = isd_as[0], trc_ver
            now = int(time.time())
            if (trc_tuple not in self.trc_requests or
                (now - self.trc_requests[trc_tuple] > self.REQUESTS_TIMEOUT)):
                trc_req = TRCRequest.from_values(isd_as, trc_ver)
                logging.info("Requesting %sv%s TRC", isd_as[0], trc_ver)
                try:
                    dst_addr = self.dns_query_topo(CERTIFICATE_SERVICE)[0]
                except SCIONServiceLookupError as e:
                    logging.warning("Sending TRC request failed: %s", e)
                    return None
                req_pkt = self._build_packet(dst_addr, payload=trc_req)
                self.send(req_pkt, dst_addr)
                self.trc_requests[trc_tuple] = now
                return None
        return trc

    def _verify_beacon(self, pcb):
        """
        Once the necessary certificate and TRC files have been found, verify the
        beacons.

        :param pcb: path segment to verify.
        :type pcb: PathSegment
        """
        assert isinstance(pcb, PathSegment)
        asm = pcb.asm(-1)
        cert_ia = asm.isd_as()
        trc = self.trust_store.get_trc(cert_ia[0], asm.p.trcVer)
        return verify_sig_chain_trc(pcb.sig_pack(), asm.p.sig, str(cert_ia),
                                    asm.chain(), trc, asm.p.trcVer)

    @abstractmethod
    def _handle_verified_beacon(self, pcb):
        """
        Once a beacon has been verified, place it into the right containers.

        :param pcb: verified path segment.
        :type pcb: PathSegment
        """
        raise NotImplementedError

    @abstractmethod
    def process_cert_chain_rep(self, cert_chain_rep):
        """
        Process the Certificate chain reply.
        """
        raise NotImplementedError

    def process_trc_rep(self, pkt):
        """
        Process the TRC reply.

        :param trc_rep: TRC reply.
        :type trc_rep: TRCReply
        """
        rep = pkt.get_payload()
        logging.info("TRC reply received for %s", rep.trc.get_isd_ver())
        self.trust_store.add_trc(rep.trc)

        rep_key = rep.trc.get_isd_ver()
        if rep_key in self.trc_requests:
            del self.trc_requests[rep_key]

    def handle_unverified_beacons(self):
        """
        Handle beacons which are waiting to be verified.
        """
        for _ in range(len(self.unverified_beacons)):
            pcb = self.unverified_beacons.popleft()
            self._try_to_verify_beacon(pcb, quiet=True)

    def process_rev_objects(self, rev_objs):
        """
        Processes revocation objects stored in Zookeeper.
        """
        for raw_obj in rev_objs:
            try:
                rev_obj = RevocationObject(raw_obj)
            except SCIONParseError as e:
                logging.error("Error processing revocation object from ZK: %s",
                              e)
                continue
            chain = self._get_if_hash_chain(rev_obj.if_id)
            if not chain:
                logging.warning("Hash-Chain for IF %d doesn't exist.",
                                rev_obj.if_id)
                return
            if chain.current_index() > rev_obj.hash_chain_idx:
                try:
                    chain.set_current_index(rev_obj.hash_chain_idx)
                    logging.info("Updated hash chain index for IF %d to %d.",
                                 rev_obj.if_id, rev_obj.hash_chain_idx)
                    self._remove_revoked_pcbs(rev_obj.rev_info, rev_obj.if_id)
                except SCIONIndexError:
                    logging.warning(
                        "Rev object for IF %d contains invalid "
                        "index: %d (1 < index < %d).", rev_obj.if_id,
                        rev_obj.hash_chain_idx,
                        len(chain) - 1)

    def _issue_revocation(self, if_id, chain):
        """
        Store a RevocationObject in ZK and send a revocation to all ERs.

        :param if_id: The interface that needs to be revoked.
        :type if_id: int
        :param chain: The hash chain corresponding to if_id.
        :type chain: :class:`lib.crypto.hash_chain.HashChain`
        """
        # Only the master BS issues revocations.
        if not self.zk.have_lock():
            return
        rev_info = RevocationInfo.from_values(chain.next_element())
        logging.info("Storing revocation in ZK.")
        rev_obj = RevocationObject.from_values(if_id, chain.current_index(),
                                               chain.next_element())
        entry_name = "%s:%s" % (chain.start_element(hex_=True),
                                chain.next_element(hex_=True))
        self.revobjs_cache.store(entry_name, rev_obj.pack())
        logging.info("Issuing revocation for IF %d.", if_id)
        # Issue revocation to all ERs.
        info = IFStateInfo.from_values(if_id, False, chain.next_element())
        pld = IFStatePayload.from_values([info])
        state_pkt = self._build_packet()
        for er in self.topology.get_all_edge_routers():
            state_pkt.addrs.dst.host = er.addr
            state_pkt.set_payload(pld.copy())
            self.send(state_pkt, er.addr)
        self._process_revocation(rev_info, if_id)

    def _send_rev_to_local_ps(self, rev_info):
        """
        Sends the given revocation to its local path server.
        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        """
        if self.zk.have_lock() and self.topology.path_servers:
            try:
                ps_addr = self.dns_query_topo(PATH_SERVICE)[0]
            except SCIONServiceLookupError:
                # If there are no local path servers, stop here.
                return
            pkt = self._build_packet(ps_addr, payload=rev_info)
            logging.info("Sending revocation to local PS.")
            self.send(pkt, ps_addr)

    def _process_revocation(self, rev_info, if_id):
        """
        Removes PCBs containing a revoked interface and sends the revocation
        to the local PS.

        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        :param if_id: The if_id to be revoked (set only for if and hop rev)
        :type if_id: int
        """
        assert isinstance(rev_info, RevocationInfo)
        logging.info("Processing revocation:\n%s", str(rev_info))
        if not if_id:
            logging.error("Trying to revoke IF with ID 0.")
            return

        self._remove_revoked_pcbs(rev_info, if_id)
        # Send revocations to local PS.
        self._send_rev_to_local_ps(rev_info)
        # Add the revocation to the downstream queue
        self.revs_to_downstream[rev_info.rev_token] = rev_info
        # Propagate the Revocation instantly
        self.handle_pcbs_propagation()

    @abstractmethod
    def _remove_revoked_pcbs(self, rev_info, if_id):
        """
        Removes the PCBs containing the revoked interface.

        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        :param if_id: The if_id to be revoked
        :type if_id: int
        """
        raise NotImplementedError

    def _pcb_list_to_remove(self, candidates, rev_info, if_id):
        """
        Calculates the list of PCBs to remove.
        Called by _remove_revoked_pcbs.

        :param candidates: Candidate PCBs.
        :type candidates: List
        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        :param if_id: The if_id to be revoked
        :type if_id: int
        """
        to_remove = []
        processed = set()
        for cand in candidates:
            if cand.id in processed:
                continue
            processed.add(cand.id)
            if if_id is not None:
                # If the beacon was received on this interface, remove it from
                # the store. We also check, if the interface didn't come up in
                # the mean time. Caveat: There is a small chance that a valid
                # beacon gets removed, in case a new beacon reaches the BS
                # through the interface, which is getting revoked, before the
                # keep-alive message updates the interface state to 'ACTIVE'.
                # However, worst, the valid beacon would get added within the
                # next propagation period.
                if (self.ifid_state[if_id].is_expired()
                        and cand.pcb.if_id == if_id):
                    to_remove.append(cand.id)
            else:  # if_id = None means that this is an AS in downstream
                rtoken = rev_info.rev_token
                for iftoken in cand.pcb.get_all_iftokens():
                    if HashChain.verify(rtoken, iftoken, self.N_TOKENS_CHECK):
                        to_remove.append(cand.id)
        return to_remove

    def _handle_if_timeouts(self):
        """
        Periodically checks each interface state and issues an if revocation, if
        no keep-alive message was received for IFID_TOUT.
        """
        while self.run_flag.is_set():
            start_time = time.time()
            for (if_id, if_state) in self.ifid_state.items():
                # Check if interface has timed-out.
                if if_state.is_expired():
                    logging.info("IF %d appears to be down.", if_id)
                    if if_id not in self.if2rev_tokens:
                        logging.error(
                            "Trying to issue revocation for " +
                            "non-existent if ID %d.", if_id)
                        continue
                    chain = self.if2rev_tokens[if_id]
                    self._issue_revocation(if_id, chain)
                    # Advance the hash chain for the corresponding IF.
                    try:
                        chain.move_to_next_element()
                    except HashChainExhausted:
                        # TODO(shitz): Add code to handle hash chain
                        # exhaustion.
                        logging.warning("HashChain for IF %s is exhausted.")
                    if_state.revoke_if_expired()
            sleep_interval(start_time, self.IF_TIMEOUT_INTERVAL,
                           "Handle IF timeouts")

    def _handle_ifstate_request(self, mgmt_pkt):
        # Only master replies to ifstate requests.
        if not self.zk.have_lock():
            return
        req = mgmt_pkt.get_payload()
        assert isinstance(req, IFStateRequest)
        logging.debug("Received ifstate req:\n%s", mgmt_pkt)
        infos = []
        if req.p.ifID == IFStateRequest.ALL_INTERFACES:
            ifid_states = self.ifid_state.items()
        elif req.p.ifID in self.ifid_state:
            ifid_states = [(req.p.ifID, self.ifid_state[req.p.ifID])]
        else:
            logging.error(
                "Received ifstate request from %s for unknown "
                "interface %s.", mgmt_pkt.addrs.src, req.p.ifID)
            return

        for (ifid, state) in ifid_states:
            # Don't include inactive interfaces in response.
            if state.is_inactive():
                continue
            chain = self._get_if_hash_chain(ifid)
            info = IFStateInfo.from_values(ifid, state.is_active(),
                                           chain.next_element())
            infos.append(info)
        if not infos and not self._quiet_startup():
            logging.warning("No IF state info to put in response.")
            return

        payload = IFStatePayload.from_values(infos)
        state_pkt = self._build_packet(mgmt_pkt.addrs.src.host,
                                       payload=payload)
        self.send(state_pkt, mgmt_pkt.addrs.src.host)
Example #23
0
class PathServer(SCIONElement, metaclass=ABCMeta):
    """
    The SCION Path Server.
    """
    SERVICE_TYPE = PATH_SERVICE
    MAX_SEG_NO = 5  # TODO: replace by config variable.
    # ZK path for incoming PATHs
    ZK_PATH_CACHE_PATH = "path_cache"
    # ZK path for incoming REVs
    ZK_REV_CACHE_PATH = "rev_cache"
    # Max number of segments per propagation packet
    PROP_LIMIT = 5
    # Max number of segments per ZK cache entry
    ZK_SHARE_LIMIT = 10
    # Time to store revocations in zookeeper
    ZK_REV_OBJ_MAX_AGE = HASHTREE_EPOCH_TIME

    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        self.down_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
        self.core_segments = PathSegmentDB(max_res_no=self.MAX_SEG_NO)
        self.pending_req = defaultdict(list)  # Dict of pending requests.
        self.pen_req_lock = threading.Lock()
        # Used when l/cPS doesn't have up/dw-path.
        self.waiting_targets = defaultdict(list)
        self.revocations = RevCache()
        # A mapping from (hash tree root of AS, IFID) to segments
        self.htroot_if2seg = ExpiringDict(1000, HASHTREE_TTL)
        self.htroot_if2seglock = Lock()
        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PATH: {
                PMT.REQUEST: self.path_resolution,
                PMT.REPLY: self.handle_path_segment_record,
                PMT.REG: self.handle_path_segment_record,
                PMT.REVOCATION: self._handle_revocation,
                PMT.SYNC: self.handle_path_segment_record,
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REQ: self.process_cert_chain_request,
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_reply,
                CertMgmtType.TRC_REPLY: self.process_trc_reply,
                CertMgmtType.TRC_REQ: self.process_trc_request,
            },
        }
        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
            },
        }
        self._segs_to_zk = deque()
        self._revs_to_zk = deque()
        self._zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                      [(self.addr.host, self._port)])
        self.zk = Zookeeper(self.topology.isd_as, PATH_SERVICE,
                            self._zkid.copy().pack(), self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.path_cache = ZkSharedCache(self.zk, self.ZK_PATH_CACHE_PATH,
                                        self._handle_paths_from_zk)
        self.rev_cache = ZkSharedCache(self.zk, self.ZK_REV_CACHE_PATH,
                                       self._rev_entries_handler)

    def worker(self):
        """
        Worker thread that takes care of reading shared paths from ZK, and
        handling master election for core servers.
        """
        worker_cycle = 1.0
        start = SCIONTime.get_time()
        was_master = False
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "cPS.worker cycle",
                           self._quiet_startup())
            start = SCIONTime.get_time()
            try:
                self.zk.wait_connected()
                self.path_cache.process()
                self.rev_cache.process()
                # Try to become a master.
                is_master = self.zk.get_lock(lock_timeout=0, conn_timeout=0)
                if is_master:
                    if not was_master:
                        logging.info("Became master")
                    self.path_cache.expire(self.config.propagation_time * 10)
                    self.rev_cache.expire(self.ZK_REV_OBJ_MAX_AGE)
                    was_master = True
                else:
                    was_master = False
            except ZkNoConnection:
                logging.warning('worker(): ZkNoConnection')
                pass
            self._update_master()
            self._propagate_and_sync()
            self._handle_pending_requests()

    def _update_master(self):
        pass

    def _rev_entries_handler(self, raw_entries):
        for raw in raw_entries:
            rev_info = RevocationInfo.from_raw(raw)
            self._remove_revoked_segments(rev_info)

    def _add_rev_mappings(self, pcb):
        """
        Add if revocation token to segment ID mappings.
        """
        segment_id = pcb.get_hops_hash()
        with self.htroot_if2seglock:
            for asm in pcb.iter_asms():
                hof = asm.pcbm(0).hof()
                egress_h = (asm.p.hashTreeRoot, hof.egress_if)
                self.htroot_if2seg.setdefault(egress_h, set()).add(segment_id)
                ingress_h = (asm.p.hashTreeRoot, hof.ingress_if)
                self.htroot_if2seg.setdefault(ingress_h, set()).add(segment_id)

    @abstractmethod
    def _handle_up_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    @abstractmethod
    def _handle_down_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    @abstractmethod
    def _handle_core_segment_record(self, pcb, **kwargs):
        raise NotImplementedError

    def _add_segment(self, pcb, seg_db, name, reverse=False):
        res = seg_db.update(pcb, reverse=reverse)
        if res == DBResult.ENTRY_ADDED:
            self._add_rev_mappings(pcb)
            logging.info("%s-Segment registered: %s", name, pcb.short_desc())
            return True
        elif res == DBResult.ENTRY_UPDATED:
            self._add_rev_mappings(pcb)
            logging.debug("%s-Segment updated: %s", name, pcb.short_desc())
        return False

    def _handle_scmp_revocation(self, pld, meta):
        rev_info = RevocationInfo.from_raw(pld.info.rev_info)
        self._handle_revocation(rev_info, meta)

    def _handle_revocation(self, rev_info, meta):
        """
        Handles a revocation of a segment, interface or hop.

        :param rev_info: The RevocationInfo object.
        """
        assert isinstance(rev_info, RevocationInfo)
        if not self._validate_revocation(rev_info):
            return
        if meta.ia[0] != self.addr.isd_as[0]:
            logging.info("Dropping revocation received from a different ISD.")
            return

        if rev_info in self.revocations:
            logging.debug("Already received revocation. Dropping...")
            return False
        self.revocations.add(rev_info)
        logging.debug("Received revocation from %s:\n%s", meta.get_addr(),
                      rev_info)
        self._revs_to_zk.append(rev_info.copy().pack())  # have to pack copy
        # Remove segments that contain the revoked interface.
        self._remove_revoked_segments(rev_info)
        # Forward revocation to other path servers.
        self._forward_revocation(rev_info, meta)

    def _remove_revoked_segments(self, rev_info):
        """
        Try the previous and next hashes as possible astokens,
        and delete any segment that matches

        :param rev_info: The revocation info
        :type rev_info: RevocationInfo
        """
        if not ConnectedHashTree.verify_epoch(rev_info.p.epoch):
            return
        (hash01, hash12) = ConnectedHashTree.get_possible_hashes(rev_info)
        if_id = rev_info.p.ifID

        with self.htroot_if2seglock:
            down_segs_removed = 0
            core_segs_removed = 0
            up_segs_removed = 0
            for h in (hash01, hash12):
                for sid in self.htroot_if2seg.pop((h, if_id), []):
                    if self.down_segments.delete(
                            sid) == DBResult.ENTRY_DELETED:
                        down_segs_removed += 1
                    if self.core_segments.delete(
                            sid) == DBResult.ENTRY_DELETED:
                        core_segs_removed += 1
                    if not self.topology.is_core_as:
                        if (self.up_segments.delete(sid) ==
                                DBResult.ENTRY_DELETED):
                            up_segs_removed += 1
            logging.info(
                "Removed segments containing IF %d: "
                "UP: %d DOWN: %d CORE: %d" %
                (if_id, up_segs_removed, down_segs_removed, core_segs_removed))

    @abstractmethod
    def _forward_revocation(self, rev_info, meta):
        """
        Forwards a revocation to other path servers that need to be notified.

        :param rev_info: The RevInfo object.
        :param meta: The MessageMeta object.
        """
        raise NotImplementedError

    def _send_path_segments(self, req, meta, up=None, core=None, down=None):
        """
        Sends path-segments to requester (depending on Path Server's location).
        """
        up = up or set()
        core = core or set()
        down = down or set()
        all_segs = up | core | down
        if not all_segs:
            logging.warning("No segments to send")
            return
        revs_to_add = self._peer_revs_for_segs(all_segs)
        pld = PathRecordsReply.from_values(
            {
                PST.UP: up,
                PST.CORE: core,
                PST.DOWN: down
            }, revs_to_add)
        self.send_meta(pld, meta)
        logging.info(
            "Sending PATH_REPLY with %d segment(s) to:%s "
            "port:%s in response to: %s",
            len(all_segs),
            meta.get_addr(),
            meta.port,
            req.short_desc(),
        )

    def _peer_revs_for_segs(self, segs):
        """Returns a list of peer revocations for segments in 'segs'."""
        def _handle_one_seg(seg):
            for asm in seg.iter_asms():
                for pcbm in asm.iter_pcbms(1):
                    hof = pcbm.hof()
                    for if_id in [hof.ingress_if, hof.egress_if]:
                        rev_info = self.revocations.get((asm.isd_as(), if_id))
                        if rev_info:
                            revs_to_add.add(rev_info.copy())
                            return

        revs_to_add = set()
        for seg in segs:
            _handle_one_seg(seg)

        return list(revs_to_add)

    def _handle_pending_requests(self):
        rem_keys = []
        # Serve pending requests.
        with self.pen_req_lock:
            for key in self.pending_req:
                to_remove = []
                for req, meta in self.pending_req[key]:
                    if self.path_resolution(req, meta, new_request=False):
                        meta.close()
                        to_remove.append((req, meta))
                # Clean state.
                for req_meta in to_remove:
                    self.pending_req[key].remove(req_meta)
                if not self.pending_req[key]:
                    rem_keys.append(key)
            for key in rem_keys:
                del self.pending_req[key]

    def _handle_paths_from_zk(self, raw_entries):
        """
        Handles cached paths through ZK, passed as a list.
        """
        for raw in raw_entries:
            recs = PathSegmentRecords.from_raw(raw)
            for type_, pcb in recs.iter_pcbs():
                seg_meta = PathSegMeta(pcb,
                                       self.continue_seg_processing,
                                       type_=type_,
                                       params={'from_zk': True})
                self.process_path_seg(seg_meta)
        logging.debug("Processed %s segments from ZK", len(raw_entries))

    def handle_path_segment_record(self, seg_recs, meta):
        """
        Handles paths received from the network.
        """
        params = self._dispatch_params(seg_recs, meta)
        # Add revocations for peer interfaces included in the path segments.
        for rev_info in seg_recs.iter_rev_infos():
            self.revocations.add(rev_info)
        # Verify pcbs and process them
        for type_, pcb in seg_recs.iter_pcbs():
            seg_meta = PathSegMeta(pcb, self.continue_seg_processing, meta,
                                   type_, params)
            self.process_path_seg(seg_meta)

    def continue_seg_processing(self, seg_meta):
        """
        For every path segment(that can be verified) received from the network
        or ZK this function gets called to continue the processing for the
        segment.
        The segment is added to pathdb and pending requests are checked.
        """
        pcb = seg_meta.seg
        type_ = seg_meta.type
        params = seg_meta.params
        self._dispatch_segment_record(type_, pcb, **params)
        self._handle_pending_requests()

    def _dispatch_segment_record(self, type_, seg, **kwargs):
        # Check that segment does not contain a revoked interface.
        if not self._validate_segment(seg):
            logging.debug("Not adding segment due to revoked interface:\n%s" %
                          seg.short_desc())
            return
        handle_map = {
            PST.UP: self._handle_up_segment_record,
            PST.CORE: self._handle_core_segment_record,
            PST.DOWN: self._handle_down_segment_record,
        }
        handle_map[type_](seg, **kwargs)

    def _validate_segment(self, seg):
        """
        Check segment for revoked upstream/downstream interfaces.

        :param seg: The PathSegment object.
        :return: False, if the path segment contains a revoked upstream/
            downstream interface (not peer). True otherwise.
        """
        for asm in seg.iter_asms():
            pcbm = asm.pcbm(0)
            for if_id in [pcbm.p.inIF, pcbm.p.outIF]:
                rev_info = self.revocations.get((asm.isd_as(), if_id))
                if rev_info:
                    logging.debug("Found revoked interface (%d) in segment "
                                  "%s." % (rev_info.p.ifID, seg.short_desc()))
                    return False
        return True

    def _dispatch_params(self, pld, meta):
        return {}

    def _propagate_and_sync(self):
        self._share_via_zk()
        self._share_revs_via_zk()

    def _gen_prop_recs(self, queue, limit=PROP_LIMIT):
        count = 0
        pcbs = defaultdict(list)
        while queue:
            count += 1
            type_, pcb = queue.popleft()
            pcbs[type_].append(pcb.copy())
            if count >= limit:
                yield (pcbs)
                count = 0
                pcbs = defaultdict(list)
        if pcbs:
            yield (pcbs)

    @abstractmethod
    def path_resolution(self, path_request, meta, new_request):
        """
        Handles all types of path request.
        """
        raise NotImplementedError

    def _handle_waiting_targets(self, pcb):
        """
        Handle any queries that are waiting for a path to any core AS in an ISD.
        """
        dst_ia = pcb.first_ia()
        if not self.is_core_as(dst_ia):
            logging.warning("Invalid waiting target, not a core AS: %s",
                            dst_ia)
            return
        self._send_waiting_queries(dst_ia[0], pcb)

    def _send_waiting_queries(self, dst_isd, pcb):
        targets = self.waiting_targets[dst_isd]
        if not targets:
            return
        path = pcb.get_path(reverse_direction=True)
        src_ia = pcb.first_ia()
        while targets:
            seg_req = targets.pop(0)
            meta = self.DefaultMeta.from_values(ia=src_ia,
                                                path=path,
                                                host=SVCType.PS_A)
            self.send_meta(seg_req, meta)
            logging.info("Waiting request (%s) sent via %s",
                         seg_req.short_desc(), pcb.short_desc())

    def _share_via_zk(self):
        if not self._segs_to_zk:
            return
        logging.info("Sharing %d segment(s) via ZK", len(self._segs_to_zk))
        for pcb_dict in self._gen_prop_recs(self._segs_to_zk,
                                            limit=self.ZK_SHARE_LIMIT):
            seg_recs = PathSegmentRecords.from_values(pcb_dict)
            self._zk_write(seg_recs.pack())

    def _share_revs_via_zk(self):
        if not self._revs_to_zk:
            return
        logging.info("Sharing %d revocation(s) via ZK", len(self._revs_to_zk))
        while self._revs_to_zk:
            self._zk_write_rev(self._revs_to_zk.popleft())

    def _zk_write(self, data):
        hash_ = SHA256.new(data).hexdigest()
        try:
            self.path_cache.store("%s-%s" % (hash_, SCIONTime.get_time()),
                                  data)
        except ZkNoConnection:
            logging.warning("Unable to store segment(s) in shared path: "
                            "no connection to ZK")

    def _zk_write_rev(self, data):
        hash_ = SHA256.new(data).hexdigest()
        try:
            self.rev_cache.store("%s-%s" % (hash_, SCIONTime.get_time()), data)
        except ZkNoConnection:
            logging.warning("Unable to store revocation(s) in shared path: "
                            "no connection to ZK")

    def run(self):
        """
        Run an instance of the Path Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="PS.worker",
                         daemon=True).start()
        super().run()
Example #24
0
class CorePathServer(PathServer):
    """
    SCION Path Server in a core AS. Stores intra ISD down-segments as well as
    core segments and forwards inter-ISD path requests to the corresponding path
    server.
    """
    def __init__(self, server_id, conf_dir, prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param str prom_export: prometheus export address.
        """
        super().__init__(server_id, conf_dir, prom_export=prom_export)
        # Sanity check that we should indeed be a core path server.
        assert self.topology.is_core_as, "This shouldn't be a local PS!"
        self._master_id = None  # Address of master core Path Server.
        self._segs_to_master = ExpiringDict(1000, 10)
        self._segs_to_prop = ExpiringDict(1000,
                                          2 * self.config.propagation_time)

    def _update_master(self):
        """
        Read master's address from shared lock, and if new master is elected
        sync it with segments.
        """
        if self.zk.have_lock():
            self._segs_to_master.clear()
            self._master_id = None
            return
        try:
            curr_master = self.zk.get_lock_holder()
        except ZkNoConnection:
            logging.warning("_update_master(): ZkNoConnection.")
            return
        if curr_master and curr_master == self._master_id:
            return
        self._master_id = curr_master
        if not curr_master:
            logging.warning("_update_master(): current master is None.")
            return
        logging.debug("New master is: %s", self._master_id)
        self._sync_master()

    def _sync_master(self):
        """
        Feed newly-elected master with segments.
        """
        assert not self.zk.have_lock()
        assert self._master_id
        # TODO(PSz): consider mechanism for avoiding a registration storm.
        core_segs = []
        # Find all core segments from remote ISDs
        for pcb in self.core_segments(full=True):
            if pcb.first_ia()[0] != self.addr.isd_as[0]:
                core_segs.append(pcb)
        # Find down-segments from local ISD.
        down_segs = self.down_segments(full=True, last_isd=self.addr.isd_as[0])
        logging.debug("Syncing with master: %s", self._master_id)
        seen_ases = set()
        for seg_type, segs in [(PST.CORE, core_segs), (PST.DOWN, down_segs)]:
            for pcb in segs:
                key = pcb.first_ia(), pcb.last_ia()
                # Send only one SCION segment for given (src, dst) pair.
                if not pcb.is_sibra() and key in seen_ases:
                    continue
                seen_ases.add(key)
                self._segs_to_master[pcb.get_hops_hash()] = (seg_type, pcb)

    def _handle_up_segment_record(self, pcb, **kwargs):
        logging.error("Core Path Server received up-segment record!")
        return set()

    def _handle_down_segment_record(self,
                                    pcb,
                                    from_master=False,
                                    from_zk=False):
        added = self._add_segment(pcb, self.down_segments, "Down")
        first_ia = pcb.first_ia()
        last_ia = pcb.last_ia()
        if first_ia == self.addr.isd_as:
            # Segment is to us, so propagate to all other core ASes within the
            # local ISD.
            self._segs_to_prop[pcb.get_hops_hash()] = (PST.DOWN, pcb)
        if (first_ia[0] == last_ia[0] == self.addr.isd_as[0] and not from_zk):
            # Sync all local down segs via zk
            self._segs_to_zk[pcb.get_hops_hash()] = (PST.DOWN, pcb)
        if added:
            return set([(last_ia, pcb.is_sibra())])
        return set()

    def _handle_core_segment_record(self,
                                    pcb,
                                    from_master=False,
                                    from_zk=False):
        """Handle registration of a core segment."""
        first_ia = pcb.first_ia()
        reverse = False
        if pcb.is_sibra() and first_ia == self.addr.isd_as:
            reverse = True
        added = self._add_segment(pcb,
                                  self.core_segments,
                                  "Core",
                                  reverse=reverse)
        if not from_zk and not from_master:
            if first_ia[0] == self.addr.isd_as[0]:
                # Local core segment, share via ZK
                self._segs_to_zk[pcb.get_hops_hash()] = (PST.CORE, pcb)
            else:
                # Remote core segment, send to master
                self._segs_to_master[pcb.get_hops_hash()] = (PST.CORE, pcb)
        if not added:
            return set()
        # Send pending requests that couldn't be processed due to the lack of
        # a core segment to the destination PS. Don't use SIBRA PCBs for that.
        if not pcb.is_sibra():
            self._handle_waiting_targets(pcb)
        ret = set([(first_ia, pcb.is_sibra())])
        if first_ia[0] != self.addr.isd_as[0]:
            # Remote core segment, signal the entire ISD
            ret.add((first_ia.any_as(), pcb.is_sibra()))
        return ret

    def _dispatch_params(self, pld, meta):
        params = {}
        if (meta.ia == self.addr.isd_as and pld.PAYLOAD_TYPE == PMT.REPLY):
            params["from_master"] = True
        return params

    def _propagate_and_sync(self):
        super()._propagate_and_sync()
        if self.zk.have_lock():
            self._prop_to_core()
        else:
            self._prop_to_master()

    def _prop_to_core(self):
        assert self.zk.have_lock()
        if not self._segs_to_prop:
            return
        logging.debug("Propagating %d segment(s) to other core ASes",
                      len(self._segs_to_prop))
        for pcbs in self._gen_prop_recs(self._segs_to_prop):
            reply = PathRecordsReply.from_values(pcbs)
            self._propagate_to_core_ases(reply)

    def _prop_to_master(self):
        assert not self.zk.have_lock()
        if not self._master_id:
            self._segs_to_master.clear()
            return
        if not self._segs_to_master:
            return
        logging.debug("Propagating %d segment(s) to master PS: %s",
                      len(self._segs_to_master), self._master_id)
        for pcbs in self._gen_prop_recs(self._segs_to_master):
            reply = PathRecordsReply.from_values(pcbs)
            self._send_to_master(reply)

    def _send_to_master(self, pld):
        """
        Send the payload to the master PS.
        """
        # XXX(kormat): Both of these should be very rare, as they are guarded
        # against in the two methods that call this one (_prop_to_master() and
        # _query_master(), but a race-condition could cause this to happen when
        # called from _query_master().
        if self.zk.have_lock():
            logging.warning("send_to_master: abandoning as we are master")
            return
        master = self._master_id
        if not master:
            logging.warning("send_to_master: abandoning as there is no master")
            return
        addr, port = master.addr(0)
        meta = self._build_meta(host=addr, port=port, reuse=True)
        self.send_meta(pld.copy(), meta)

    def _query_master(self, dst_ia, logger, src_ia=None, flags=()):
        """
        Query master for a segment.
        """
        if self.zk.have_lock() or not self._master_id:
            return
        src_ia = src_ia or self.addr.isd_as
        # XXX(kormat) Requests forwarded to the master CPS should be cache-only, as they only happen
        # in the case where a core segment is missing or a local down-segment is missing, and there
        # is nothing to query if the master CPS doesn't already have the information.
        # This has the side-effect of preventing query loops that could occur when two non-master
        # CPSes each believe the other is the master, for example.
        sflags = set(flags)
        sflags.add(PATH_FLAG_CACHEONLY)
        flags = tuple(sflags)
        req = PathSegmentReq.from_values(src_ia, dst_ia, flags=flags)
        logger.debug("Asking master (%s) for segment: %s" %
                     (self._master_id, req.short_desc()))
        self._send_to_master(req)

    def _propagate_to_core_ases(self, rep_recs):
        """
        Propagate 'pkt' to other core ASes.
        """
        for isd_as in self._core_ases[self.addr.isd_as[0]]:
            if isd_as == self.addr.isd_as:
                continue
            csegs = self.core_segments(first_ia=isd_as,
                                       last_ia=self.addr.isd_as)
            if not csegs:
                logging.warning(
                    "Cannot propagate %s to AS %s. No path available." %
                    (rep_recs.NAME, isd_as))
                continue
            cseg = csegs[0].get_path(reverse_direction=True)
            meta = self._build_meta(ia=isd_as,
                                    path=cseg,
                                    host=SVCType.PS_A,
                                    reuse=True)
            self.send_meta(rep_recs.copy(), meta)

    def path_resolution(self, req, meta, new_request=True, logger=None):
        """
        Handle generic type of a path request.
        new_request informs whether a pkt is a new request (True), or is a
        pending request (False).
        Return True when resolution succeeded, False otherwise.
        """
        if logger is None:
            logger = self.get_request_logger(req, meta)
        dst_ia = req.dst_ia()
        if new_request:
            logger.info("PATH_REQ received")
            REQS_TOTAL.labels(**self._labels).inc()
        if dst_ia == self.addr.isd_as:
            logger.warning("Dropping request: requested DST is local AS")
            return False
        # dst as==0 means any core AS in the specified ISD
        dst_is_core = self.is_core_as(dst_ia) or dst_ia[1] == 0
        if dst_is_core:
            core_segs = self._resolve_core(req, meta, dst_ia, new_request,
                                           req.flags(), logger)
            down_segs = set()
        else:
            core_segs, down_segs = self._resolve_not_core(
                req, meta, dst_ia, new_request, req.flags(), logger)

        if not (core_segs | down_segs):
            if new_request:
                logger.debug("Segs to %s not found." % dst_ia)
            return False

        self._send_path_segments(req,
                                 meta,
                                 logger,
                                 core=core_segs,
                                 down=down_segs)
        return True

    def _resolve_core(self, req, meta, dst_ia, new_request, flags, logger):
        """
        Dst is core AS.
        """
        sibra = PATH_FLAG_SIBRA in flags
        params = {"last_ia": self.addr.isd_as}
        params["sibra"] = sibra
        params.update(dst_ia.params())
        core_segs = set(self.core_segments(**params))
        if not core_segs and new_request and PATH_FLAG_CACHEONLY not in flags:
            # Segments not found and it is a new request.
            self.pending_req[(dst_ia, sibra)].append((req, meta, logger))
            # If dst is in remote ISD then a segment may be kept by master.
            if dst_ia[0] != self.addr.isd_as[0]:
                self._query_master(dst_ia, logger, flags=flags)
        return core_segs

    def _resolve_not_core(self, seg_req, meta, dst_ia, new_request, flags,
                          logger):
        """
        Dst is regular AS.
        """
        sibra = PATH_FLAG_SIBRA in flags
        core_segs = set()
        down_segs = set()
        # Check if there exists any down-segs to dst.
        tmp_down_segs = self.down_segments(last_ia=dst_ia, sibra=sibra)
        if not tmp_down_segs and new_request and PATH_FLAG_CACHEONLY not in flags:
            self._resolve_not_core_failed(seg_req, meta, dst_ia, flags, logger)

        for dseg in tmp_down_segs:
            dseg_ia = dseg.first_ia()
            if (dseg_ia == self.addr.isd_as
                    or seg_req.src_ia()[0] != self.addr.isd_as[0]):
                # If it's a direct down-seg, or if it's a remote query, there's
                # no need to include core-segs
                down_segs.add(dseg)
                continue
            # Now try core segments that connect to down segment.
            tmp_core_segs = self.core_segments(first_ia=dseg_ia,
                                               last_ia=self.addr.isd_as,
                                               sibra=sibra)
            if not tmp_core_segs and new_request and PATH_FLAG_CACHEONLY not in flags:
                # Core segment not found and it is a new request.
                self.pending_req[(dseg_ia, sibra)].append(
                    (seg_req, meta, logger))
                if dst_ia[0] != self.addr.isd_as[0]:
                    # Master may know a segment.
                    self._query_master(dseg_ia, logger, flags=flags)
            elif tmp_core_segs:
                down_segs.add(dseg)
                core_segs.update(tmp_core_segs)
        return core_segs, down_segs

    def _resolve_not_core_failed(self, seg_req, meta, dst_ia, flags, logger):
        """
        Execute after _resolve_not_core() cannot resolve a new request, due to
        lack of corresponding down segment(s).
        This must not be executed for a pending request.
        """
        sibra = PATH_FLAG_SIBRA in flags
        self.pending_req[(dst_ia, sibra)].append((seg_req, meta, logger))
        if dst_ia[0] == self.addr.isd_as[0]:
            # Master may know down segment as dst is in local ISD.
            self._query_master(dst_ia, logger, flags=flags)
            return

        # Dst is in a remote ISD, ask any core AS from there. Don't use a SIBRA
        # segment, even if the request has the SIBRA flag set, as this is just
        # for basic internal communication.
        csegs = self.core_segments(first_isd=dst_ia[0],
                                   last_ia=self.addr.isd_as)
        if csegs:
            cseg = csegs[0]
            path = cseg.get_path(reverse_direction=True)
            dst_ia = cseg.first_ia()
            logger.info("Down-Segment request for different ISD, "
                        "forwarding request to CPS in %s via %s" %
                        (dst_ia, cseg.short_desc()))
            meta = self._build_meta(ia=dst_ia,
                                    path=path,
                                    host=SVCType.PS_A,
                                    reuse=True)
            self.send_meta(seg_req, meta)
        else:
            # If no core segment was available, add request to waiting targets.
            logger.info("Waiting for core segment to ISD %s", dst_ia[0])
            self.waiting_targets[dst_ia[0]].append((seg_req, logger))
            # Ask for any segment to dst_isd
            self._query_master(dst_ia.any_as(), logger)

    def _forward_revocation(self, rev_info, meta):
        # Propagate revocation to other core ASes if:
        # 1) The revoked interface belongs to this AS, or
        # 2) the revocation was received from a non-core AS in this ISD, or
        # 3) the revocation was forked from a BR and it originated from a
        #    different ISD.
        rev_isd_as = rev_info.isd_as()
        if (rev_isd_as == self.addr.isd_as
                or (meta.ia not in self._core_ases[self.addr.isd_as[0]])
                or (meta.ia == self.addr.isd_as
                    and rev_isd_as[0] != self.addr.isd_as[0])):
            logging.debug("Propagating revocation to other cores: %s" %
                          rev_info.short_desc())
            self._propagate_to_core_ases(rev_info)

    def _init_metrics(self):
        super()._init_metrics()
        SEGS_TO_MASTER.labels(**self._labels).set(0)
        SEGS_TO_PROP.labels(**self._labels).set(0)

    def _update_metrics(self):
        super()._update_metrics()
        if self._labels:
            SEGS_TO_MASTER.labels(**self._labels).set(len(
                self._segs_to_master))
            SEGS_TO_PROP.labels(**self._labels).set(len(self._segs_to_prop))
Example #25
0
class SCIONElement(object):
    """
    Base class for the different kind of servers the SCION infrastructure
    provides.

    :ivar `Topology` topology: the topology of the AS as seen by the server.
    :ivar `Config` config:
        the configuration of the AS in which the server is located.
    :ivar dict ifid2br: map of interface ID to RouterElement.
    :ivar `SCIONAddr` addr: the server's address.
    """
    SERVICE_TYPE = None
    STARTUP_QUIET_PERIOD = STARTUP_QUIET_PERIOD
    USE_TCP = False
    # Timeout for TRC or Certificate requests.
    TRC_CC_REQ_TIMEOUT = 3

    def __init__(self, server_id, conf_dir, public=None, bind=None, spki_cache_dir=GEN_CACHE_PATH,
                 prom_export=None):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        :param list public:
            (host_addr, port) of the element's public address
            (i.e. the address visible to other network elements).
        :param list bind:
            (host_addr, port) of the element's bind address, if any
            (i.e. the address the element uses to identify itself to the local
            operating system, if it differs from the public address due to NAT).
        :param str spki_cache_dir:
            Path for caching TRCs and certificate chains.
        :param str prom_export:
            String of the form 'addr:port' specifying the prometheus endpoint.
            If no string is provided, no metrics are exported.
        """
        self.id = server_id
        self.conf_dir = conf_dir
        self.ifid2br = {}
        self.topology = Topology.from_file(
            os.path.join(self.conf_dir, TOPO_FILE))
        # Labels attached to every exported metric.
        self._labels = {"server_id": self.id, "isd_as": str(self.topology.isd_as)}
        # Must be over-ridden by child classes:
        self.CTRL_PLD_CLASS_MAP = {}
        self.SCMP_PLD_CLASS_MAP = {}
        self.public = public
        self.bind = bind
        if self.SERVICE_TYPE:
            own_config = self.topology.get_own_config(self.SERVICE_TYPE,
                                                      server_id)
            if public is None:
                self.public = own_config.public
            if bind is None:
                self.bind = own_config.bind
        self.init_ifid2br()
        self.trust_store = TrustStore(self.conf_dir, spki_cache_dir, self.id, self._labels)
        self.total_dropped = 0
        self._core_ases = defaultdict(list)  # Mapping ISD_ID->list of core ASes
        self.init_core_ases()
        self.run_flag = threading.Event()
        self.run_flag.set()
        self.stopped_flag = threading.Event()
        self.stopped_flag.clear()
        self._in_buf = queue.Queue(MAX_QUEUE)
        self._socks = SocketMgr()
        self._startup = time.time()
        if self.USE_TCP:
            self._DefaultMeta = TCPMetadata
        else:
            self._DefaultMeta = UDPMetadata
        self.unverified_segs = ExpiringDict(500, 60 * 60)
        self.unv_segs_lock = threading.RLock()
        self.requested_trcs = {}
        self.req_trcs_lock = threading.Lock()
        self.requested_certs = {}
        self.req_certs_lock = threading.Lock()
        # TODO(jonghoonkwon): Fix me to setup sockets for multiple public addresses
        host_addr, self._port = self.public[0]
        self.addr = SCIONAddr.from_values(self.topology.isd_as, host_addr)
        if prom_export:
            self._export_metrics(prom_export)
            self._init_metrics()
        self._setup_sockets(True)
        lib_sciond.init(os.path.join(SCIOND_API_SOCKDIR, "sd%s.sock" % self.addr.isd_as))

    def _load_as_conf(self):
        return Config.from_file(os.path.join(self.conf_dir, AS_CONF_FILE))

    def _setup_sockets(self, init):
        """
        Setup incoming socket and register with dispatcher
        """
        self._tcp_sock = None
        self._tcp_new_conns = queue.Queue(MAX_QUEUE)  # New TCP connections.
        if self._port is None:
            # No scion socket desired.
            return
        svc = SERVICE_TO_SVC_A.get(self.SERVICE_TYPE)
        # Setup TCP "accept" socket.
        self._setup_tcp_accept_socket(svc)
        # Setup UDP socket
        if self.bind:
            # TODO(jonghoonkwon): Fix me to setup socket for a proper bind address,
            # if the element has more than one bind addresses
            host_addr, b_port = self.bind[0]
            b_addr = SCIONAddr.from_values(self.topology.isd_as, host_addr)
            self._udp_sock = ReliableSocket(
                reg=(self.addr, self._port, init, svc), bind_ip=(b_addr, b_port))
        else:
            self._udp_sock = ReliableSocket(
                reg=(self.addr, self._port, init, svc))
        if not self._udp_sock.registered:
            self._udp_sock = None
            return
        if self._labels:
            CONNECTED_TO_DISPATCHER.labels(**self._labels).set(1)
        self._port = self._udp_sock.port
        self._socks.add(self._udp_sock, self.handle_recv)

    def _setup_tcp_accept_socket(self, svc):
        if not self.USE_TCP:
            return
        MAX_TRIES = 40
        for i in range(MAX_TRIES):
            try:
                self._tcp_sock = SCIONTCPSocket()
                self._tcp_sock.setsockopt(SockOpt.SOF_REUSEADDR)
                self._tcp_sock.set_recv_tout(TCP_ACCEPT_POLLING_TOUT)
                self._tcp_sock.bind((self.addr, self._port), svc=svc)
                self._tcp_sock.listen()
                break
            except SCIONTCPError as e:
                logging.warning("TCP: Cannot connect to LWIP socket: %s" % e)
            time.sleep(1)  # Wait for dispatcher
        else:
            logging.critical("TCP: cannot init TCP socket.")
            kill_self()

    def init_ifid2br(self):
        for br in self.topology.border_routers:
            for if_id in br.interfaces:
                self.ifid2br[if_id] = br

    def init_core_ases(self):
        """
        Initializes dict of core ASes.
        """
        for trc in self.trust_store.get_trcs():
            self._core_ases[trc.isd] = trc.get_core_ases()

    def is_core_as(self, isd_as=None):
        if not isd_as:
            isd_as = self.addr.isd_as
        return isd_as in self._core_ases[isd_as[0]]

    def _update_core_ases(self, trc):
        """
        When a new trc is received, this function is called to
        update the core ases map
        """
        self._core_ases[trc.isd] = trc.get_core_ases()

    def get_border_addr(self, ifid):
        br = self.ifid2br[ifid]
        addr_idx = br.interfaces[ifid].addr_idx
        br_addr, br_port = br.int_addrs[addr_idx].public[0]
        return br_addr, br_port

    def handle_msg_meta(self, msg, meta):
        """
        Main routine to handle incoming SCION messages.
        """
        if isinstance(meta, SCMPMetadata):
            handler = self._get_scmp_handler(meta.pkt)
        else:
            handler = self._get_ctrl_handler(msg)
        if not handler:
            logging.error("handler not found: %s", msg)
            return
        try:
            # SIBRA operates on parsed packets.
            if (isinstance(meta, UDPMetadata) and msg.type() == PayloadClass.SIBRA):
                handler(meta.pkt)
            else:
                handler(msg, meta)
        except SCIONBaseError:
            log_exception("Error handling message:\n%s" % msg)

    def _check_trc_cert_reqs(self):
        check_cyle = 1.0
        while self.run_flag.is_set():
            start = time.time()
            self._check_cert_reqs()
            self._check_trc_reqs()
            sleep_interval(start, check_cyle, "Elem._check_trc_cert_reqs cycle")

    def _check_trc_reqs(self):
        """
        Checks if TRC requests timeout and resends requests if so.
        """
        with self.req_trcs_lock:
            now = time.time()
            for (isd, ver), (req_time, meta) in self.requested_trcs.items():
                if now - req_time >= self.TRC_CC_REQ_TIMEOUT:
                    trc_req = TRCRequest.from_values(isd, ver, cache_only=True)
                    meta = meta or self._get_cs()
                    req_id = mk_ctrl_req_id()
                    logging.info("Re-Requesting TRC from %s: %s [id: %016x]",
                                 meta, trc_req.short_desc(), req_id)
                    self.send_meta(CtrlPayload(CertMgmt(trc_req), req_id=req_id), meta)
                    self.requested_trcs[(isd, ver)] = (time.time(), meta)
                    if self._labels:
                        PENDING_TRC_REQS_TOTAL.labels(**self._labels).set(len(self.requested_trcs))

    def _check_cert_reqs(self):
        """
        Checks if certificate requests timeout and resends requests if so.
        """
        with self.req_certs_lock:
            now = time.time()
            for (isd_as, ver), (req_time, meta) in self.requested_certs.items():
                if now - req_time >= self.TRC_CC_REQ_TIMEOUT:
                    cert_req = CertChainRequest.from_values(isd_as, ver, cache_only=True)
                    meta = meta or self._get_cs()
                    req_id = mk_ctrl_req_id()
                    logging.info("Re-Requesting CERTCHAIN from %s: %s [id: %016x]",
                                 meta, cert_req.short_desc(), req_id)
                    self.send_meta(CtrlPayload(CertMgmt(cert_req), req_id=req_id), meta)
                    self.requested_certs[(isd_as, ver)] = (time.time(), meta)
                    if self._labels:
                        PENDING_CERT_REQS_TOTAL.labels(**self._labels).set(
                            len(self.requested_certs))

    def _process_path_seg(self, seg_meta, req_id=None):
        """
        When a pcb or path segment is received, this function is called to
        find missing TRCs and certs and request them.
        :param seg_meta: PathSegMeta object that contains pcb/path segment
        """
        meta_str = str(seg_meta.meta) if seg_meta.meta else "ZK"
        req_str = "[id: %016x]" % req_id if req_id else ""
        logging.debug("Handling PCB from %s: %s %s",
                      meta_str, seg_meta.seg.short_desc(), req_str)
        with self.unv_segs_lock:
            # Close the meta of the previous seg_meta, if there was one.
            prev_meta = self.unverified_segs.get(seg_meta.id)
            if prev_meta and prev_meta.meta:
                prev_meta.meta.close()
            self.unverified_segs[seg_meta.id] = seg_meta
            if self._labels:
                UNV_SEGS_TOTAL.labels(**self._labels).set(len(self.unverified_segs))
        # Find missing TRCs and certificates
        missing_trcs = self._missing_trc_versions(seg_meta.trc_vers)
        missing_certs = self._missing_cert_versions(seg_meta.cert_vers)
        # Update missing TRCs/certs map
        seg_meta.missing_trcs.update(missing_trcs)
        seg_meta.missing_certs.update(missing_certs)
        # If all necessary TRCs/certs available, try to verify
        if seg_meta.verifiable():
            self._try_to_verify_seg(seg_meta)
            return
        # Otherwise request missing trcs, certs
        self._request_missing_trcs(seg_meta)
        self._request_missing_certs(seg_meta)
        if seg_meta.meta:
            seg_meta.meta.close()

    def _try_to_verify_seg(self, seg_meta):
        """
        If this pcb/path segment can be verified, call the function
        to process a verified pcb/path segment
        """
        try:
            self._verify_path_seg(seg_meta)
        except SCIONVerificationError as e:
            logging.error("Signature verification failed for %s: %s" %
                          (seg_meta.seg.short_id(), e))
            return
        with self.unv_segs_lock:
            self.unverified_segs.pop(seg_meta.id, None)
            if self._labels:
                UNV_SEGS_TOTAL.labels(**self._labels).set(len(self.unverified_segs))
        if seg_meta.meta:
            seg_meta.meta.close()
        seg_meta.callback(seg_meta)

    def _get_cs(self):
        """
        Lookup certificate servers address and return meta.
        """
        try:
            addr, port = self.dns_query_topo(CERTIFICATE_SERVICE)[0]
        except SCIONServiceLookupError as e:
            logging.warning("Lookup for certificate service failed: %s", e)
            return None
        return UDPMetadata.from_values(host=addr, port=port)

    def _request_missing_trcs(self, seg_meta):
        """
        For all missing TRCs which are missing to verify this pcb/path segment,
        request them. Request is sent to certificate server, if the
        pcb/path segment was received by zk. Otherwise the sender of this
        pcb/path segment is asked.
        """
        missing_trcs = set()
        with seg_meta.miss_trc_lock:
            missing_trcs = seg_meta.missing_trcs.copy()
        if not missing_trcs:
            return
        for isd, ver in missing_trcs:
            with self.req_trcs_lock:
                req_time, meta = self.requested_trcs.get((isd, ver), (None, None))
                if meta:
                    # There is already an outstanding request for the missing TRC
                    # from somewhere else than than the local CS
                    if seg_meta.meta:
                        # Update the stored meta with the latest known server that has the TRC.
                        self.requested_trcs[(isd, ver)] = (req_time, seg_meta.meta)
                    continue
                if req_time and not seg_meta.meta:
                    # There is already an outstanding request for the missing TRC
                    # to the local CS and we don't have a new meta.
                    continue
            trc_req = TRCRequest.from_values(isd, ver, cache_only=True)
            meta = seg_meta.meta or self._get_cs()
            if not meta:
                logging.error("Couldn't find a CS to request TRC for PCB %s",
                              seg_meta.seg.short_id())
                continue
            req_id = mk_ctrl_req_id()
            logging.info("Requesting %sv%s TRC from %s, for PCB %s [id: %016x]",
                         isd, ver, meta, seg_meta.seg.short_id(), req_id)
            with self.req_trcs_lock:
                self.requested_trcs[(isd, ver)] = (time.time(), seg_meta.meta)
                if self._labels:
                    PENDING_TRC_REQS_TOTAL.labels(**self._labels).set(len(self.requested_trcs))
            self.send_meta(CtrlPayload(CertMgmt(trc_req), req_id=req_id), meta)

    def _request_missing_certs(self, seg_meta):
        """
        For all missing CCs which are missing to verify this pcb/path segment,
        request them. Request is sent to certificate server, if the
        pcb/path segment was received by zk. Otherwise the sender of this
        pcb/path segment is asked.
        """
        missing_certs = set()
        with seg_meta.miss_cert_lock:
            missing_certs = seg_meta.missing_certs.copy()
        if not missing_certs:
            return
        for isd_as, ver in missing_certs:
            with self.req_certs_lock:
                req_time, meta = self.requested_certs.get((isd_as, ver), (None, None))
                if meta:
                    # There is already an outstanding request for the missing cert
                    # from somewhere else than than the local CS
                    if seg_meta.meta:
                        # Update the stored meta with the latest known server that has the cert.
                        self.requested_certs[(isd_as, ver)] = (req_time, seg_meta.meta)
                    continue
                if req_time and not seg_meta.meta:
                    # There is already an outstanding request for the missing cert
                    # to the local CS and we don't have a new meta.
                    continue
            cert_req = CertChainRequest.from_values(isd_as, ver, cache_only=True)
            meta = seg_meta.meta or self._get_cs()
            if not meta:
                logging.error("Couldn't find a CS to request CERTCHAIN for PCB %s",
                              seg_meta.seg.short_id())
                continue
            req_id = mk_ctrl_req_id()
            logging.info("Requesting %sv%s CERTCHAIN from %s for PCB %s [id: %016x]",
                         isd_as, ver, meta, seg_meta.seg.short_id(), req_id)
            with self.req_certs_lock:
                self.requested_certs[(isd_as, ver)] = (time.time(), seg_meta.meta)
                if self._labels:
                    PENDING_CERT_REQS_TOTAL.labels(**self._labels).set(len(self.requested_certs))
            self.send_meta(CtrlPayload(CertMgmt(cert_req), req_id=req_id), meta)

    def _missing_trc_versions(self, trc_versions):
        """
        Check which intermediate trcs are missing and return their versions.
        :returns: the missing TRCs'
        :rtype set
        """
        missing_trcs = set()
        for isd, versions in trc_versions.items():
            # If not local TRC, only request versions contained in ASMarkings
            if isd is not self.topology.isd_as[0]:
                for ver in versions:
                    if self.trust_store.get_trc(isd, ver) is None:
                        missing_trcs.add((isd, ver))
                continue
            # Local TRC
            max_req_ver = max(versions)
            max_local_ver = self.trust_store.get_trc(isd)
            lower_ver = 0
            if max_local_ver is None:
                # This should never happen
                logging.critical("Local TRC not found!")
                kill_self()
            lower_ver = max_local_ver.version + 1
            for ver in range(lower_ver, max_req_ver + 1):
                missing_trcs.add((isd, ver))
        return missing_trcs

    def _missing_cert_versions(self, cert_versions):
        """
        Check which and certificates are missing return their versions.
        :returns: the missing certs' versions
        :rtype set
        """
        missing_certs = set()
        for isd_as, versions in cert_versions.items():
            for ver in versions:
                if self.trust_store.get_cert(isd_as, ver) is None:
                    missing_certs.add((isd_as, ver))
        return missing_certs

    def process_trc_reply(self, cpld, meta):
        """
        Process the TRC reply.
        :param rep: TRC reply.
        :type rep: TRCReply.
        """
        meta.close()
        cmgt = cpld.union
        rep = cmgt.union
        assert isinstance(rep, TRCReply), type(rep)
        isd, ver = rep.trc.get_isd_ver()
        logging.info("TRC reply received for %sv%s from %s [id: %s]",
                     isd, ver, meta, cpld.req_id_str())
        self.trust_store.add_trc(rep.trc, True)
        # Update core ases for isd this trc belongs to
        max_local_ver = self.trust_store.get_trc(rep.trc.isd)
        if max_local_ver.version == rep.trc.version:
            self._update_core_ases(rep.trc)
        with self.req_trcs_lock:
            self.requested_trcs.pop((isd, ver), None)
            if self._labels:
                PENDING_TRC_REQS_TOTAL.labels(**self._labels).set(len(self.requested_trcs))
        # Send trc to CS
        if meta.get_addr().isd_as != self.addr.isd_as:
            cs_meta = self._get_cs()
            self.send_meta(CtrlPayload(CertMgmt(rep)), cs_meta)
            cs_meta.close()
        # Remove received TRC from map
        self._check_segs_with_rec_trc(isd, ver)

    def _check_segs_with_rec_trc(self, isd, ver):
        """
        When a trc reply is received, this method is called to check which
        segments can be verified. For all segments that can be verified,
        the processing is continued.
        """
        with self.unv_segs_lock:
            for seg_meta in list(self.unverified_segs.values()):
                with seg_meta.miss_trc_lock:
                    seg_meta.missing_trcs.discard((isd, ver))
                # If all required trcs and certs are received
                if seg_meta.verifiable():
                    self._try_to_verify_seg(seg_meta)

    def process_trc_request(self, cpld, meta):
        """Process a TRC request."""
        cmgt = cpld.union
        req = cmgt.union
        assert isinstance(req, TRCRequest), type(req)
        isd, ver = req.isd_as()[0], req.p.version
        logging.info("TRC request received for %sv%s from %s [id: %s]" %
                     (isd, ver, meta, cpld.req_id_str()))
        trc = self.trust_store.get_trc(isd, ver)
        if trc:
            self.send_meta(
                CtrlPayload(CertMgmt(TRCReply.from_values(trc)), req_id=cpld.req_id),
                meta)
        else:
            logging.warning("Could not find requested TRC %sv%s [id: %s]" %
                            (isd, ver, cpld.req_id_str()))

    def process_cert_chain_reply(self, cpld, meta):
        """Process a certificate chain reply."""
        cmgt = cpld.union
        rep = cmgt.union
        assert isinstance(rep, CertChainReply), type(rep)
        meta.close()
        isd_as, ver = rep.chain.get_leaf_isd_as_ver()
        logging.info("Cert chain reply received for %sv%s from %s [id: %s]",
                     isd_as, ver, meta, cpld.req_id_str())
        self.trust_store.add_cert(rep.chain, True)
        with self.req_certs_lock:
            self.requested_certs.pop((isd_as, ver), None)
            if self._labels:
                PENDING_CERT_REQS_TOTAL.labels(**self._labels).set(len(self.requested_certs))
        # Send cc to CS
        if meta.get_addr().isd_as != self.addr.isd_as:
            cs_meta = self._get_cs()
            self.send_meta(CtrlPayload(CertMgmt(rep)), cs_meta)
            cs_meta.close()
        # Remove received cert chain from map
        self._check_segs_with_rec_cert(isd_as, ver)

    def _check_segs_with_rec_cert(self, isd_as, ver):
        """
        When a CC reply is received, this method is called to check which
        segments can be verified. For all segments that can be verified,
        the processing is continued.
        """
        with self.unv_segs_lock:
            for seg_meta in list(self.unverified_segs.values()):
                with seg_meta.miss_cert_lock:
                    seg_meta.missing_certs.discard((isd_as, ver))
                # If all required trcs and certs are received.
                if seg_meta.verifiable():
                    self._try_to_verify_seg(seg_meta)

    def process_cert_chain_request(self, cpld, meta):
        """Process a certificate chain request."""
        cmgt = cpld.union
        req = cmgt.union
        assert isinstance(req, CertChainRequest), type(req)
        isd_as, ver = req.isd_as(), req.p.version
        logging.info("Cert chain request received for %sv%s from %s [id: %s]" %
                     (isd_as, ver, meta, cpld.req_id_str()))
        cert = self.trust_store.get_cert(isd_as, ver)
        if cert:
            self.send_meta(
                CtrlPayload(CertMgmt(CertChainReply.from_values(cert)), req_id=cpld.req_id),
                meta)
        else:
            logging.warning("Could not find requested certificate %sv%s [id: %s]" %
                            (isd_as, ver, cpld.req_id_str()))

    def _verify_path_seg(self, seg_meta):
        """
        Signature verification for all AS markings within this pcb/path segment.
        This function is called, when all TRCs and CCs used within this pcb/path
        segment are available.
        """
        seg = seg_meta.seg
        exp_time = seg.get_expiration_time()
        for i, asm in enumerate(seg.iter_asms()):
            cert_ia = asm.isd_as()
            trc = self.trust_store.get_trc(cert_ia[0], asm.p.trcVer)
            chain = self.trust_store.get_cert(asm.isd_as(), asm.p.certVer)
            self._verify_exp_time(exp_time, chain)
            verify_chain_trc(cert_ia, chain, trc)
            seg.verify(chain.as_cert.subject_sig_key_raw, i)

    def _verify_exp_time(self, exp_time, chain):
        """
        Verify that certificate chain cover the expiration time.
        :raises SCIONVerificationError
        """
        # chain is only verifiable if TRC.exp_time >= CoreCert.exp_time >= LeafCert.exp_time
        if chain.as_cert.expiration_time < exp_time:
            raise SCIONVerificationError(
                "Certificate chain %sv%s expires before path segment" % chain.get_leaf_isd_as_ver())

    def _get_ctrl_handler(self, msg):
        pclass = msg.type()
        try:
            type_map = self.CTRL_PLD_CLASS_MAP[pclass]
        except KeyError:
            logging.error("Control payload class not supported: %s\n%s", pclass, msg)
            return None
        ptype = msg.inner_type()
        try:
            return type_map[ptype]
        except KeyError:
            logging.error("%s control payload type not supported: %s\n%s", pclass, ptype, msg)
        return None

    def _get_scmp_handler(self, pkt):
        scmp = pkt.l4_hdr
        try:
            type_map = self.SCMP_PLD_CLASS_MAP[scmp.class_]
        except KeyError:
            logging.error("SCMP class not supported: %s(%s)\n%s",
                          scmp.class_, SCMPClass.to_str(scmp.class_), pkt)
            return None
        try:
            return type_map[scmp.type]
        except KeyError:
            logging.error("SCMP %s type not supported: %s(%s)\n%s", scmp.type,
                          scmp.class_, scmp_type_name(scmp.class_, scmp.type), pkt)
        return None

    def _parse_packet(self, packet):
        try:
            pkt = SCIONL4Packet(packet)
        except SCMPError as e:
            self._scmp_parse_error(packet, e)
            return None
        except SCIONBaseError:
            log_exception("Error parsing packet: %s" % hex_str(packet),
                          level=logging.ERROR)
            return None
        try:
            pkt.validate(len(packet))
        except SCMPError as e:
            self._scmp_validate_error(pkt, e)
            return None
        except SCIONChecksumFailed:
            logging.debug("Dropping packet due to failed checksum:\n%s", pkt)
        return pkt

    def _scmp_parse_error(self, packet, e):
        HDR_TYPE_OFFSET = 6
        if packet[HDR_TYPE_OFFSET] == L4Proto.SCMP:
            # Ideally, never respond to an SCMP error with an SCMP error.
            # However, if parsing failed, we can (at best) only determine if
            # it's an SCMP packet, so just drop SCMP packets on parse error.
            logging.warning("Dropping SCMP packet due to parse error. %s", e)
            return
        # For now, none of these can be properly handled, so just log and drop
        # the packet. In the future, the "x Not Supported" errors might be
        # handlable in the case of deprecating old versions.
        DROP = SCMPBadVersion, SCMPBadSrcType, SCMPBadDstType
        assert isinstance(e, DROP), type(e)
        logging.warning("Dropping packet due to parse error: %s", e)

    def _scmp_validate_error(self, pkt, e):
        if pkt.cmn_hdr.next_hdr == L4Proto.SCMP and pkt.ext_hdrs[0].error:
            # Never respond to an SCMP error with an SCMP error.
            logging.info(
                "Dropping SCMP error packet due to validation error. %s", e)
            return
        if isinstance(e, (SCMPBadIOFOffset, SCMPBadHOFOffset)):
            # Can't handle normally, as the packet isn't reversible.
            reply = self._scmp_bad_path_metadata(pkt, e)
        else:
            logging.warning("Error: %s", type(e))
            reply = pkt.reversed_copy()
            args = ()
            if isinstance(e, SCMPUnspecified):
                args = (str(e),)
            elif isinstance(e, (SCMPOversizePkt, SCMPBadPktLen)):
                args = (e.args[1],)  # the relevant MTU.
            elif isinstance(e, (SCMPTooManyHopByHop, SCMPBadExtOrder,
                                SCMPBadHopByHop)):
                args = e.args
                if isinstance(e, SCMPBadExtOrder):
                    # Delete the problematic extension.
                    del reply.ext_hdrs[args[0]]
            reply.convert_to_scmp_error(self.addr, e.CLASS, e.TYPE, pkt, *args)
        if pkt.addrs.src.isd_as == self.addr.isd_as:
            # No path needed for a local reply.
            reply.path = SCIONPath()
        next_hop, port = self.get_first_hop(reply)
        reply.update()
        self.send(reply, next_hop, port)

    def _scmp_bad_path_metadata(self, pkt, e):
        """
        Handle a packet with an invalid IOF/HOF offset in the common header.

        As the path can't be used, a response can only be sent if the source is
        local (as that doesn't require a path).
        """
        if pkt.addrs.src.isd_as != self.addr.isd_as:
            logging.warning(
                "Invalid path metadata in packet from "
                "non-local source, dropping: %s\n%s\n%s\n%s",
                e, pkt.cmn_hdr, pkt.addrs, pkt.path)
            return
        reply = copy.deepcopy(pkt)
        # Remove existing path before reversing.
        reply.path = SCIONPath()
        reply.reverse()
        reply.convert_to_scmp_error(self.addr, e.CLASS, e.TYPE, pkt)
        reply.update()
        logging.warning(
            "Invalid path metadata in packet from "
            "local source, sending SCMP error: %s\n%s\n%s\n%s",
            e, pkt.cmn_hdr, pkt.addrs, pkt.path)
        return reply

    def get_first_hop(self, spkt):
        """
        Returns first hop addr of down-path or end-host addr.
        """
        return self._get_first_hop(spkt.path, spkt.addrs.dst, spkt.ext_hdrs)

    def _get_first_hop(self, path, dst, ext_hdrs=()):
        if_id = self._ext_first_hop(ext_hdrs)
        if if_id is None:
            if len(path) == 0:
                return self._empty_first_hop(dst)
            if_id = path.get_fwd_if()
        if if_id in self.ifid2br:
            return self.get_border_addr(if_id)
        logging.error("Unable to find first hop:\n%s", path)
        return None, None

    def _ext_first_hop(self, ext_hdrs):
        for hdr in ext_hdrs:
            if_id = hdr.get_next_ifid()
            if if_id is not None:
                return if_id

    def _empty_first_hop(self, dst):
        if dst.isd_as != self.addr.isd_as:
            logging.error("Packet to remote AS w/o path, dst: %s", dst)
            return None, None
        host = dst.host
        if host.TYPE == AddrType.SVC:
            host = self.dns_query_topo(SVC_TO_SERVICE[host.addr])[0][0]
        return host, SCION_UDP_EH_DATA_PORT

    def _build_packet(self, dst_host=None, path=None, ext_hdrs=(),
                      dst_ia=None, payload=None, dst_port=0):
        if dst_host is None:
            dst_host = HostAddrNone()
        if dst_ia is None:
            dst_ia = self.addr.isd_as
        if path is None:
            path = SCIONPath()
        if payload is None:
            payload = PayloadRaw()
        dst_addr = SCIONAddr.from_values(dst_ia, dst_host)
        cmn_hdr, addr_hdr = build_base_hdrs(dst_addr, self.addr)
        udp_hdr = SCIONUDPHeader.from_values(
            self.addr, self._port, dst_addr, dst_port)
        return SCIONL4Packet.from_values(
            cmn_hdr, addr_hdr, path, ext_hdrs, udp_hdr, payload)

    def send(self, packet, dst, dst_port):
        """
        Send *packet* to *dst* (to port *dst_port*) using the local socket.
        Calling ``packet.pack()`` should return :class:`bytes`, and
        ``dst.__str__()`` should return a string representing an IP address.

        :param packet: the packet to be sent to the destination.
        :param str dst: the destination IP address.
        :param int dst_port: the destination port number.
        """
        assert not isinstance(packet.addrs.src.host, HostAddrNone), type(packet.addrs.src.host)
        assert not isinstance(packet.addrs.dst.host, HostAddrNone), type(packet.addrs.dst.host)
        assert isinstance(packet, SCIONBasePacket), type(packet)
        assert isinstance(dst_port, int), type(dst_port)
        if not self._udp_sock:
            return False
        return self._udp_sock.send(packet.pack(), (dst, dst_port))

    def send_meta(self, msg, meta, next_hop_port=None):
        if isinstance(meta, TCPMetadata):
            assert not next_hop_port, next_hop_port
            return self._send_meta_tcp(msg, meta)
        elif isinstance(meta, SockOnlyMetadata):
            assert not next_hop_port, next_hop_port
            return meta.sock.send(msg)
        elif isinstance(meta, UDPMetadata):
            dst_port = meta.port
        else:
            logging.error("Unsupported metadata: %s" % meta.__name__)
            return False

        pkt = self._build_packet(meta.host, meta.path, meta.ext_hdrs,
                                 meta.ia, msg, dst_port)
        if not next_hop_port:
            next_hop_port = self.get_first_hop(pkt)
        if next_hop_port == (None, None):
            logging.error("Can't find first hop, dropping packet\n%s", pkt)
            return False
        return self.send(pkt, *next_hop_port)

    def _send_meta_tcp(self, msg, meta):
        if not meta.sock:
            tcp_sock = self._tcp_sock_from_meta(meta)
            meta.sock = tcp_sock
            self._tcp_conns_put(tcp_sock)
        return meta.sock.send_msg(msg.pack())

    def _tcp_sock_from_meta(self, meta):
        assert meta.host
        dst = meta.get_addr()
        first_ip, first_port = self._get_first_hop(meta.path, dst)
        active = True
        try:
            # Create low-level TCP socket and connect
            sock = SCIONTCPSocket()
            sock.bind((self.addr, 0))
            sock.connect(dst, meta.port, meta.path, first_ip, first_port,
                         flags=meta.flags)
        except SCIONTCPError:
            log_exception("TCP: connection init error, marking socket inactive")
            sock = None
            active = False
        # Create and return TCPSocketWrapper
        return TCPSocketWrapper(sock, dst, meta.path, active)

    def _tcp_conns_put(self, sock):
        dropped = 0
        while True:
            try:
                self._tcp_new_conns.put(sock, block=False)
            except queue.Full:
                old_sock = self._tcp_new_conns.get_nowait()
                old_sock.close()
                logging.error("TCP: _tcp_new_conns is full. Closing old socket")
                dropped += 1
            else:
                break
        if dropped > 0:
            logging.warning("%d TCP connection(s) dropped" % dropped)

    def run(self):
        """
        Main routine to receive packets and pass them to
        :func:`handle_request()`.
        """
        self._tcp_start()
        threading.Thread(
            target=thread_safety_net, args=(self.packet_recv,),
            name="Elem.packet_recv", daemon=True).start()
        try:
            self._packet_process()
        except SCIONBaseError:
            log_exception("Error processing packet.")
        finally:
            self.stop()

    def packet_put(self, packet, addr, sock):
        """
        Try to put incoming packet in queue
        If queue is full, drop oldest packet in queue
        """
        msg, meta = self._get_msg_meta(packet, addr, sock)
        if msg is None:
            return
        self._in_buf_put((msg, meta))

    def _in_buf_put(self, item):
        dropped = 0
        while True:
            try:
                self._in_buf.put(item, block=False)
                if self._labels:
                    PKT_BUF_BYTES.labels(**self._labels).inc(len(item[0]))
            except queue.Full:
                msg, _ = self._in_buf.get_nowait()
                dropped += 1
                if self._labels:
                    PKTS_DROPPED_TOTAL.labels(**self._labels).inc()
                    PKT_BUF_BYTES.labels(**self._labels).dec(len(msg))
            else:
                break
            finally:
                if self._labels:
                    PKT_BUF_TOTAL.labels(**self._labels).set(self._in_buf.qsize())
        if dropped > 0:
            self.total_dropped += dropped
            logging.warning("%d packet(s) dropped (%d total dropped so far)",
                            dropped, self.total_dropped)

    def _get_msg_meta(self, packet, addr, sock):
        pkt = self._parse_packet(packet)
        if not pkt:
            logging.error("Cannot parse packet:\n%s" % packet)
            return None, None
        # Create metadata:
        rev_pkt = pkt.reversed_copy()
        # Skip OneHopPathExt (if exists)
        exts = []
        for e in rev_pkt.ext_hdrs:
            if not isinstance(e, OneHopPathExt):
                exts.append(e)
        if rev_pkt.l4_hdr.TYPE == L4Proto.UDP:
            meta = UDPMetadata.from_values(ia=rev_pkt.addrs.dst.isd_as,
                                           host=rev_pkt.addrs.dst.host,
                                           path=rev_pkt.path,
                                           ext_hdrs=exts,
                                           port=rev_pkt.l4_hdr.dst_port)
        elif rev_pkt.l4_hdr.TYPE == L4Proto.SCMP:
            meta = SCMPMetadata.from_values(ia=rev_pkt.addrs.dst.isd_as,
                                            host=rev_pkt.addrs.dst.host,
                                            path=rev_pkt.path,
                                            ext_hdrs=exts)

        else:
            logging.error("Cannot create meta for: %s" % pkt)
            return None, None

        # FIXME(PSz): for now it is needed by SIBRA service.
        meta.pkt = pkt
        try:
            pkt.parse_payload()
        except SCIONParseError as e:
            logging.error("Cannot parse payload\n  Error: %s\n  Pkt: %s", e, pkt)
            return None, meta
        return pkt.get_payload(), meta

    def handle_accept(self, sock):
        """
        Callback to handle a ready listening socket
        """
        s = sock.accept()
        if not s:
            logging.error("accept failed")
            return
        self._socks.add(s, self.handle_recv)

    def handle_recv(self, sock):
        """
        Callback to handle a ready recving socket
        """
        packet, addr = sock.recv()
        if packet is None:
            self._socks.remove(sock)
            sock.close()
            if sock == self._udp_sock:
                self._udp_sock = None
                if self._labels:
                    CONNECTED_TO_DISPATCHER.labels(**self._labels).set(0)
            return
        self.packet_put(packet, addr, sock)

    def packet_recv(self):
        """
        Read packets from sockets, and put them into a :class:`queue.Queue`.
        """
        while self.run_flag.is_set():
            if not self._udp_sock:
                self._setup_sockets(False)
            for sock, callback in self._socks.select_(timeout=0.1):
                callback(sock)
            self._tcp_socks_update()
        self._socks.close()
        self.stopped_flag.set()

    def _packet_process(self):
        """
        Read packets from a :class:`queue.Queue`, and process them.
        """
        while self.run_flag.is_set():
            try:
                msg, meta = self._in_buf.get(timeout=1.0)
                if self._labels:
                    PKT_BUF_BYTES.labels(**self._labels).dec(len(msg))
                    PKT_BUF_TOTAL.labels(**self._labels).set(self._in_buf.qsize())
                self.handle_msg_meta(msg, meta)
            except queue.Empty:
                continue

    def _tcp_start(self):
        if not self.USE_TCP:
            return
        if not self._tcp_sock:
            logging.warning("TCP: accept socket is unset, port:%d", self._port)
            return
        threading.Thread(
            target=thread_safety_net, args=(self._tcp_accept_loop,),
            name="Elem._tcp_accept_loop", daemon=True).start()

    def _tcp_accept_loop(self):
        while self.run_flag.is_set():
            try:
                logging.debug("TCP: waiting for connections")
                self._tcp_conns_put(TCPSocketWrapper(*self._tcp_sock.accept()))
                logging.debug("TCP: accepted connection")
            except SCIONTCPTimeout:
                pass
            except SCIONTCPError:
                log_exception("TCP: error on accept()")
                logging.error("TCP: leaving the accept loop")
                break
        try:
            self._tcp_sock.close()
        except SCIONTCPError:
            log_exception("TCP: error on closing _tcp_sock")

    def _tcp_socks_update(self):
        if not self.USE_TCP:
            return
        self._socks.remove_inactive()
        self._tcp_add_waiting()

    def _tcp_add_waiting(self):
        while True:
            try:
                self._socks.add(self._tcp_new_conns.get_nowait(),
                                self._tcp_handle_recv)
            except queue.Empty:
                break

    def _tcp_handle_recv(self, sock):
        """
        Callback to handle a ready recving socket
        """
        msg, meta = sock.get_msg_meta()
        logging.debug("tcp_handle_recv:%s, %s", msg, meta)
        if msg is None and meta is None:
            self._socks.remove(sock)
            sock.close()
            return
        if msg:
            self._in_buf_put((msg, meta))

    def _tcp_clean(self):
        if not hasattr(self, "_tcp_sock") or not self._tcp_sock:
            return
        # Close all TCP sockets.
        while not self._tcp_new_conns.empty():
            try:
                tcp_sock = self._tcp_new_conns.get_nowait()
            except queue.Empty:
                break
            tcp_sock.close()

    def stop(self):
        """Shut down the daemon thread."""
        # Signal that the thread should stop
        self.run_flag.clear()
        # Wait for the thread to finish
        self.stopped_flag.wait(5)
        # Close tcp sockets.
        self._tcp_clean()

    def _quiet_startup(self):
        return (time.time() - self._startup) < self.STARTUP_QUIET_PERIOD

    def dns_query_topo(self, qname):
        """
        Query dns for an answer. If the answer is empty, or an error occurs then
        return the relevant topology entries instead.

        :param str qname: Service to query for.
        """
        assert qname in SERVICE_TYPES
        service_map = {
            BEACON_SERVICE: self.topology.beacon_servers,
            CERTIFICATE_SERVICE: self.topology.certificate_servers,
            PATH_SERVICE: self.topology.path_servers,
            SIBRA_SERVICE: self.topology.sibra_servers,
        }
        # Generate fallback from local topology
        results = []
        for srv in service_map[qname]:
            addr, port = srv.public[0]
            results.append((addr, port))
        # FIXME(kormat): replace with new discovery service when that's ready.
        if not results:
            # No results from local toplogy either
            raise SCIONServiceLookupError("No %s servers found" % qname)
        return results

    def _verify_revocation_for_asm(self, rev_info, as_marking, verify_all=True):
        """
        Verifies a revocation for a given AS marking.

        :param rev_info: The RevocationInfo object.
        :param as_marking: The ASMarking object.
        :param verify_all: If true, verify all PCBMs (including peers),
            otherwise only verify the up/down hop.
        :return: True, if the revocation successfully revokes an upstream
            interface in the AS marking, False otherwise.
        """
        if rev_info.isd_as() != as_marking.isd_as():
            return False
        if not ConnectedHashTree.verify(rev_info, as_marking.p.hashTreeRoot):
            logging.error("Revocation verification failed. %s", rev_info)
            return False
        for pcbm in as_marking.iter_pcbms():
            if rev_info.p.ifID in [pcbm.hof().ingress_if, pcbm.hof().egress_if]:
                return True
            if not verify_all:
                break
        return False

    def _build_meta(self, ia=None, host=None, path=None, port=0, reuse=False,
                    one_hop=False):
        if ia is None:
            ia = self.addr.isd_as
        if path is None:
            path = SCIONPath()
        if not one_hop:
            return self._DefaultMeta.from_values(ia, host, path, port=port,
                                                 reuse=reuse)
        # One hop path extension in handled in a different way in TCP and UDP
        if self._DefaultMeta == TCPMetadata:
            return TCPMetadata.from_values(ia, host, path, port=port, reuse=reuse,
                                           flags=TCPFlags.ONEHOPPATH)
        return UDPMetadata.from_values(ia, host, path, port=port, reuse=reuse,
                                       ext_hdrs=[OneHopPathExt()])

    def _export_metrics(self, export_addr):
        """
        Starts an HTTP server endpoint for prometheus to scrape.
        """
        addr, port = export_addr.split(":")
        port = int(port)
        addr = addr.strip("[]")
        logging.info("Exporting metrics on %s", export_addr)
        start_http_server(port, addr=addr)

    def _init_metrics(self):
        """
        Initializes all metrics to 0. Subclasses should initialize their metrics here and
        must call the super method.
        """
        PKT_BUF_TOTAL.labels(**self._labels).set(0)
        PKT_BUF_BYTES.labels(**self._labels).set(0)
        PKTS_DROPPED_TOTAL.labels(**self._labels).inc(0)
        UNV_SEGS_TOTAL.labels(**self._labels).set(0)
        PENDING_TRC_REQS_TOTAL.labels(**self._labels).set(0)
        PENDING_CERT_REQS_TOTAL.labels(**self._labels).set(0)
        CONNECTED_TO_DISPATCHER.labels(**self._labels).set(0)

    def _get_path_via_sciond(self, isd_as, flush=False):
        flags = lib_sciond.PathRequestFlags(flush=flush)
        start = time.time()
        while time.time() - start < API_TOUT:
            try:
                path_entries = lib_sciond.get_paths(isd_as, flags=flags)
            except lib_sciond.SCIONDLibError as e:
                logging.error("Error during path lookup: %s" % e)
                continue
            if path_entries:
                return path_entries[0].path()
        logging.warning("Unable to get path to %s from SCIOND.", isd_as)
        return None
Example #26
0
class BeaconServer(SCIONElement, metaclass=ABCMeta):
    """
    The SCION PathConstructionBeacon Server.

    Attributes:
        if2rev_tokens: Contains the currently used revocation token
            hash-chain for each interface.
    """
    SERVICE_TYPE = BEACON_SERVICE
    # Amount of time units a HOF is valid (time unit is EXP_TIME_UNIT).
    HOF_EXP_TIME = 63
    # Timeout for TRC or Certificate requests.
    REQUESTS_TIMEOUT = 10
    # ZK path for incoming PCBs
    ZK_PCB_CACHE_PATH = "pcb_cache"
    # ZK path for revocations.
    ZK_REVOCATIONS_PATH = "rev_cache"
    # Time revocation objects are cached in memory (in seconds).
    ZK_REV_OBJ_MAX_AGE = HASHTREE_EPOCH_TIME
    # Interval to checked for timed out interfaces.
    IF_TIMEOUT_INTERVAL = 1

    def __init__(self, server_id, conf_dir):
        """
        :param str server_id: server identifier.
        :param str conf_dir: configuration directory.
        """
        super().__init__(server_id, conf_dir)
        # TODO: add 2 policies
        self.path_policy = PathPolicy.from_file(
            os.path.join(conf_dir, PATH_POLICY_FILE))
        self.unverified_beacons = deque()
        self.trc_requests = {}
        self.trcs = {}
        sig_key_file = get_sig_key_file_path(self.conf_dir)
        self.signing_key = base64.b64decode(read_file(sig_key_file))
        self.of_gen_key = PBKDF2(self.config.master_as_key, b"Derive OF Key")
        self.hashtree_gen_key = PBKDF2(self.config.master_as_key,
                                       b"Derive hashtree Key")
        logging.info(self.config.__dict__)
        self._hash_tree = None
        self._hash_tree_lock = Lock()
        self._next_tree = None
        self._init_hash_tree()
        self.ifid_state = {}
        for ifid in self.ifid2br:
            self.ifid_state[ifid] = InterfaceState()
        self.ifid_state_lock = RLock()
        self.CTRL_PLD_CLASS_MAP = {
            PayloadClass.PCB: {
                None: self.handle_pcb
            },
            PayloadClass.IFID: {
                None: self.handle_ifid_packet
            },
            PayloadClass.CERT: {
                CertMgmtType.CERT_CHAIN_REPLY: self.process_cert_chain_rep,
                CertMgmtType.TRC_REPLY: self.process_trc_rep,
            },
            PayloadClass.PATH: {
                PMT.IFSTATE_REQ: self._handle_ifstate_request,
                PMT.REVOCATION: self._handle_revocation,
            },
        }
        self.SCMP_PLD_CLASS_MAP = {
            SCMPClass.PATH: {
                SCMPPathClass.REVOKED_IF: self._handle_scmp_revocation,
            },
        }

        zkid = ZkID.from_values(self.addr.isd_as, self.id,
                                [(self.addr.host, self._port)]).pack()
        self.zk = Zookeeper(self.addr.isd_as, BEACON_SERVICE, zkid,
                            self.topology.zookeepers)
        self.zk.retry("Joining party", self.zk.party_setup)
        self.incoming_pcbs = deque()
        self.pcb_cache = ZkSharedCache(self.zk, self.ZK_PCB_CACHE_PATH,
                                       self.process_pcbs)
        self.revobjs_cache = ZkSharedCache(self.zk, self.ZK_REVOCATIONS_PATH,
                                           self.process_rev_objects)
        self.local_rev_cache = ExpiringDict(
            1000, HASHTREE_EPOCH_TIME + HASHTREE_EPOCH_TOLERANCE)
        self.local_rev_cache_lock = Lock()

    def _init_hash_tree(self):
        ifs = list(self.ifid2br.keys())
        self._hash_tree = ConnectedHashTree(self.addr.isd_as, ifs,
                                            self.hashtree_gen_key)

    def _get_ht_proof(self, if_id):
        with self._hash_tree_lock:
            return self._hash_tree.get_proof(if_id)

    def _get_ht_root(self):
        with self._hash_tree_lock:
            return self._hash_tree.get_root()

    def propagate_downstream_pcb(self, pcb):
        """
        Propagates the beacon to all children.

        :param pcb: path segment.
        :type pcb: PathSegment
        """
        for r in self.topology.child_border_routers:
            if not r.interface.to_if_id:
                continue
            new_pcb, meta = self._mk_prop_pcb_meta(pcb.copy(),
                                                   r.interface.isd_as,
                                                   r.interface.if_id)
            if not new_pcb:
                continue
            self.send_meta(new_pcb, meta)
            logging.info("Downstream PCB propagated to %s via IF %s",
                         r.interface.isd_as, r.interface.if_id)

    def _mk_prop_pcb_meta(self, pcb, dst_ia, egress_if):
        ts = pcb.get_timestamp()
        asm = self._create_asm(pcb.p.ifID, egress_if, ts, pcb.last_hof())
        if not asm:
            return None, None
        pcb.add_asm(asm)
        pcb.sign(self.signing_key)
        one_hop_path = self._create_one_hop_path(egress_if)
        if self.DefaultMeta == TCPMetadata:
            return pcb, self.DefaultMeta.from_values(ia=dst_ia,
                                                     host=SVCType.BS_A,
                                                     path=one_hop_path,
                                                     flags=TCPFlags.ONEHOPPATH)
        return pcb, UDPMetadata.from_values(ia=dst_ia,
                                            host=SVCType.BS_A,
                                            path=one_hop_path,
                                            ext_hdrs=[OneHopPathExt()])

    def _create_one_hop_path(self, egress_if):
        ts = int(SCIONTime.get_time())
        info = InfoOpaqueField.from_values(ts, self.addr.isd_as[0], hops=2)
        hf1 = HopOpaqueField.from_values(self.HOF_EXP_TIME, 0, egress_if)
        hf1.set_mac(self.of_gen_key, ts, None)
        # Return a path where second HF is empty.
        return SCIONPath.from_values(info, [hf1, HopOpaqueField()])

    def _mk_if_info(self, if_id):
        """
        Small helper method to make it easier to deal with ingress/egress
        interface being 0 while building ASMarkings.
        """
        d = {"remote_ia": ISD_AS.from_values(0, 0), "remote_if": 0, "mtu": 0}
        if not if_id:
            return d
        br = self.ifid2br[if_id]
        d["remote_ia"] = br.interface.isd_as
        d["remote_if"] = br.interface.to_if_id
        d["mtu"] = br.interface.mtu
        return d

    @abstractmethod
    def handle_pcbs_propagation(self):
        """
        Main loop to propagate received beacons.
        """
        raise NotImplementedError

    def handle_pcb(self, pcb, meta):
        """Receives beacon and stores it for processing."""
        pcb.p.ifID = meta.path.get_hof().ingress_if
        if not self.path_policy.check_filters(pcb):
            return
        self.incoming_pcbs.append(pcb)
        meta.close()
        entry_name = "%s-%s" % (pcb.get_hops_hash(hex=True), time.time())
        try:
            self.pcb_cache.store(entry_name, pcb.copy().pack())
        except ZkNoConnection:
            logging.error("Unable to store PCB in shared cache: "
                          "no connection to ZK")

    def handle_ext(self, pcb):
        """
        Handle beacon extensions.
        """
        # Handle PCB extensions:
        if pcb.is_sibra():
            logging.debug("%s", pcb.sibra_ext)

    @abstractmethod
    def process_pcbs(self, pcbs, raw=True):
        """
        Processes new beacons and appends them to beacon list.
        """
        raise NotImplementedError

    def process_pcb_queue(self):
        pcbs = []
        while self.incoming_pcbs:
            pcbs.append(self.incoming_pcbs.popleft())
        self.process_pcbs(pcbs, raw=False)
        logging.debug("Processed %d pcbs from incoming queue", len(pcbs))

    @abstractmethod
    def register_segments(self):
        """
        Registers paths according to the received beacons.
        """
        raise NotImplementedError

    def _create_asm(self, in_if, out_if, ts, prev_hof):
        pcbms = list(self._create_pcbms(in_if, out_if, ts, prev_hof))
        if not pcbms:
            return None
        chain = self._get_my_cert()
        _, cert_ver = chain.get_leaf_isd_as_ver()
        return ASMarking.from_values(self.addr.isd_as,
                                     self._get_my_trc().version, cert_ver,
                                     pcbms, self._get_ht_root(),
                                     self.topology.mtu, chain)

    def _create_pcbms(self, in_if, out_if, ts, prev_hof):
        up_pcbm = self._create_pcbm(in_if, out_if, ts, prev_hof)
        if not up_pcbm:
            return
        yield up_pcbm
        for br in sorted(self.topology.peer_border_routers):
            in_if = br.interface.if_id
            with self.ifid_state_lock:
                if (not self.ifid_state[in_if].is_active()
                        and not self._quiet_startup()):
                    logging.warning('Peer ifid:%d inactive (not added).',
                                    in_if)
                    continue
            peer_pcbm = self._create_pcbm(in_if,
                                          out_if,
                                          ts,
                                          up_pcbm.hof(),
                                          xover=True)
            if peer_pcbm:
                yield peer_pcbm

    def _create_pcbm(self, in_if, out_if, ts, prev_hof, xover=False):
        in_info = self._mk_if_info(in_if)
        if in_info["remote_ia"].int() and not in_info["remote_if"]:
            return None
        out_info = self._mk_if_info(out_if)
        if out_info["remote_ia"].int() and not out_info["remote_if"]:
            return None
        hof = HopOpaqueField.from_values(self.HOF_EXP_TIME,
                                         in_if,
                                         out_if,
                                         xover=xover)
        hof.set_mac(self.of_gen_key, ts, prev_hof)
        return PCBMarking.from_values(in_info["remote_ia"],
                                      in_info["remote_if"], in_info["mtu"],
                                      out_info["remote_ia"],
                                      out_info["remote_if"], hof)

    def _terminate_pcb(self, pcb):
        """
        Copies a PCB, terminates it and adds the segment ID.

        Terminating a PCB means adding a opaque field with the egress IF set
        to 0, i.e., there is no AS to forward a packet containing this path
        segment to.
        """
        pcb = pcb.copy()
        asm = self._create_asm(pcb.p.ifID, 0, pcb.get_timestamp(),
                               pcb.last_hof())
        if not asm:
            return None
        pcb.add_asm(asm)
        return pcb

    def handle_ifid_packet(self, pld, meta):
        """
        Update the interface state for the corresponding interface.

        :param pld: The IFIDPayload.
        :type pld: IFIDPayload
        """
        ifid = pld.p.relayIF
        with self.ifid_state_lock:
            if ifid not in self.ifid_state:
                raise SCIONKeyError("Invalid IF %d in IFIDPayload" % ifid)
            br = self.ifid2br[ifid]
            br.interface.to_if_id = pld.p.origIF
            prev_state = self.ifid_state[ifid].update()
            if prev_state == InterfaceState.INACTIVE:
                logging.info("IF %d activated", ifid)
            elif prev_state in [
                    InterfaceState.TIMED_OUT, InterfaceState.REVOKED
            ]:
                logging.info("IF %d came back up.", ifid)
            if not prev_state == InterfaceState.ACTIVE:
                if self.zk.have_lock():
                    # Inform BRs about the interface coming up.
                    state_info = IFStateInfo.from_values(
                        ifid, True, self._get_ht_proof(ifid))
                    pld = IFStatePayload.from_values([state_info])
                    for br in self.topology.get_all_border_routers():
                        meta = UDPMetadata.from_values(host=br.addr,
                                                       port=br.port)
                        self.send_meta(pld.copy(), meta, (br.addr, br.port))

    def run(self):
        """
        Run an instance of the Beacon Server.
        """
        threading.Thread(target=thread_safety_net,
                         args=(self.worker, ),
                         name="BS.worker",
                         daemon=True).start()
        # https://github.com/netsec-ethz/scion/issues/308:
        threading.Thread(target=thread_safety_net,
                         args=(self._handle_if_timeouts, ),
                         name="BS._handle_if_timeouts",
                         daemon=True).start()
        threading.Thread(target=thread_safety_net,
                         args=(self._create_next_tree, ),
                         name="BS._create_next_tree",
                         daemon=True).start()
        super().run()

    def _create_next_tree(self):
        last_ttl_window = 0
        while self.run_flag.is_set():
            start = time.time()
            cur_ttl_window = ConnectedHashTree.get_ttl_window()
            time_to_sleep = (ConnectedHashTree.get_time_till_next_ttl() -
                             HASHTREE_UPDATE_WINDOW)
            if cur_ttl_window == last_ttl_window:
                time_to_sleep += HASHTREE_TTL
            if time_to_sleep > 0:
                sleep_interval(start, time_to_sleep, "BS._create_next_tree",
                               self._quiet_startup())

            # at this point, there should be <= HASHTREE_UPDATE_WINDOW
            # seconds left in current ttl
            logging.info("Started computing hashtree for next ttl")
            last_ttl_window = ConnectedHashTree.get_ttl_window()

            ifs = list(self.ifid2br.keys())
            tree = ConnectedHashTree.get_next_tree(self.addr.isd_as, ifs,
                                                   self.hashtree_gen_key)
            with self._hash_tree_lock:
                self._next_tree = tree

    def _maintain_hash_tree(self):
        """
        Maintain the hashtree. Update the the windows in the connected tree
        """
        with self._hash_tree_lock:
            if self._next_tree is not None:
                self._hash_tree.update(self._next_tree)
                self._next_tree = None
            else:
                logging.critical("Did not create hashtree in time; dying")
                kill_self()
        logging.info("New Hash Tree TTL beginning")

    def worker(self):
        """
        Worker thread that takes care of reading shared PCBs from ZK, and
        propagating PCBS/registering paths when master.
        """
        last_propagation = last_registration = 0
        last_ttl_window = ConnectedHashTree.get_ttl_window()
        worker_cycle = 1.0
        was_master = False
        start = time.time()
        while self.run_flag.is_set():
            sleep_interval(start, worker_cycle, "BS.worker cycle",
                           self._quiet_startup())
            start = time.time()
            try:
                self.process_pcb_queue()
                self.handle_unverified_beacons()
                self.zk.wait_connected()
                self.pcb_cache.process()
                self.revobjs_cache.process()
                self.handle_rev_objs()

                cur_ttl_window = ConnectedHashTree.get_ttl_window()
                if cur_ttl_window != last_ttl_window:
                    self._maintain_hash_tree()
                    last_ttl_window = cur_ttl_window

                if not self.zk.get_lock(lock_timeout=0, conn_timeout=0):
                    was_master = False
                    continue

                if not was_master:
                    self._became_master()
                    was_master = True
                self.pcb_cache.expire(self.config.propagation_time * 10)
                self.revobjs_cache.expire(self.ZK_REV_OBJ_MAX_AGE)
            except ZkNoConnection:
                continue
            now = time.time()
            if now - last_propagation >= self.config.propagation_time:
                self.handle_pcbs_propagation()
                last_propagation = now
            if (self.config.registers_paths and
                    now - last_registration >= self.config.registration_time):
                try:
                    self.register_segments()
                except SCIONKeyError as e:
                    logging.error("Register_segments: %s", e)
                    pass
                last_registration = now

    def _became_master(self):
        """
        Called when a BS becomes the new master. Resets some state that will be
        rebuilt over time.
        """
        # Reset all timed-out and revoked interfaces to inactive.
        with self.ifid_state_lock:
            for (_, ifstate) in self.ifid_state.items():
                if not ifstate.is_active():
                    ifstate.reset()

    def _try_to_verify_beacon(self, pcb, quiet=False):
        """
        Try to verify a beacon.

        :param pcb: path segment to verify.
        :type pcb: PathSegment
        """
        assert isinstance(pcb, PathSegment)
        asm = pcb.asm(-1)
        if self._check_trc(asm.isd_as(), asm.p.trcVer):
            if self._verify_beacon(pcb):
                self._handle_verified_beacon(pcb)
            else:
                logging.warning("Invalid beacon. %s", pcb)
        else:
            if not quiet:
                logging.warning("Certificate(s) or TRC missing for pcb: %s",
                                pcb.short_desc())
            self.unverified_beacons.append(pcb)

    @abstractmethod
    def _check_trc(self, isd_as, trc_ver):
        """
        Return True or False whether the necessary Certificate and TRC files are
        found.

        :param ISD_AS isd_is: ISD-AS identifier.
        :param int trc_ver: TRC file version.
        """
        raise NotImplementedError

    def _get_my_trc(self):
        return self.trust_store.get_trc(self.addr.isd_as[0])

    def _get_my_cert(self):
        return self.trust_store.get_cert(self.addr.isd_as)

    def _get_trc(self, isd_as, trc_ver):
        """
        Get TRC from local storage or memory.

        :param ISD_AS isd_as: ISD-AS identifier.
        :param int trc_ver: TRC file version.
        """
        trc = self.trust_store.get_trc(isd_as[0], trc_ver)
        if not trc:
            # Requesting TRC file from cert server
            trc_tuple = isd_as[0], trc_ver
            now = int(time.time())
            if (trc_tuple not in self.trc_requests or
                (now - self.trc_requests[trc_tuple] > self.REQUESTS_TIMEOUT)):
                trc_req = TRCRequest.from_values(isd_as, trc_ver)
                logging.info("Requesting %sv%s TRC", isd_as[0], trc_ver)
                try:
                    addr, port = self.dns_query_topo(CERTIFICATE_SERVICE)[0]
                except SCIONServiceLookupError as e:
                    logging.warning("Sending TRC request failed: %s", e)
                    return None
                meta = UDPMetadata.from_values(host=addr, port=port)
                self.send_meta(trc_req, meta)
                self.trc_requests[trc_tuple] = now
                return None
        return trc

    def _verify_beacon(self, pcb):
        """
        Once the necessary certificate and TRC files have been found, verify the
        beacons.

        :param pcb: path segment to verify.
        :type pcb: PathSegment
        """
        assert isinstance(pcb, PathSegment)
        asm = pcb.asm(-1)
        cert_ia = asm.isd_as()
        trc = self.trust_store.get_trc(cert_ia[0], asm.p.trcVer)
        return verify_sig_chain_trc(pcb.sig_pack(), asm.p.sig, str(cert_ia),
                                    asm.chain(), trc, asm.p.trcVer)

    @abstractmethod
    def _handle_verified_beacon(self, pcb):
        """
        Once a beacon has been verified, place it into the right containers.

        :param pcb: verified path segment.
        :type pcb: PathSegment
        """
        raise NotImplementedError

    @abstractmethod
    def process_cert_chain_rep(self, cert_chain_rep, meta):
        """
        Process the Certificate chain reply.
        """
        raise NotImplementedError

    def process_trc_rep(self, rep, meta):
        """
        Process the TRC reply.

        :param rep: TRC reply.
        :type rep: TRCReply
        """
        logging.info("TRC reply received for %s", rep.trc.get_isd_ver())
        self.trust_store.add_trc(rep.trc)

        rep_key = rep.trc.get_isd_ver()
        if rep_key in self.trc_requests:
            del self.trc_requests[rep_key]

    def handle_unverified_beacons(self):
        """
        Handle beacons which are waiting to be verified.
        """
        for _ in range(len(self.unverified_beacons)):
            pcb = self.unverified_beacons.popleft()
            self._try_to_verify_beacon(pcb, quiet=True)

    def process_rev_objects(self, rev_infos):
        """
        Processes revocation infos stored in Zookeeper.
        """
        with self.local_rev_cache_lock:
            for raw in rev_infos:
                try:
                    rev_info = RevocationInfo.from_raw(raw)
                except SCIONParseError as e:
                    logging.error(
                        "Error processing revocation info from ZK: %s", e)
                    continue
                self.local_rev_cache[rev_info] = rev_info.copy()

    def _issue_revocation(self, if_id):
        """
        Store a RevocationInfo in ZK and send a revocation to all BRs.

        :param if_id: The interface that needs to be revoked.
        :type if_id: int
        """
        # Only the master BS issues revocations.
        if not self.zk.have_lock():
            return
        rev_info = self._get_ht_proof(if_id)
        logging.error("Issuing revocation for IF %d.", if_id)
        # Issue revocation to all BRs.
        info = IFStateInfo.from_values(if_id, False, rev_info)
        pld = IFStatePayload.from_values([info])
        for br in self.topology.get_all_border_routers():
            meta = UDPMetadata.from_values(host=br.addr, port=br.port)
            self.send_meta(pld.copy(), meta, (br.addr, br.port))
        self._process_revocation(rev_info)
        self._send_rev_to_local_ps(rev_info)

    def _send_rev_to_local_ps(self, rev_info):
        """
        Sends the given revocation to its local path server.
        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        """
        if self.zk.have_lock() and self.topology.path_servers:
            try:
                addr, port = self.dns_query_topo(PATH_SERVICE)[0]
            except SCIONServiceLookupError:
                # If there are no local path servers, stop here.
                return
            logging.info("Sending revocation to local PS.")
            meta = UDPMetadata.from_values(host=addr, port=port)
            self.send_meta(rev_info.copy(), meta)

    def _handle_scmp_revocation(self, pld, meta):
        rev_info = RevocationInfo.from_raw(pld.info.rev_info)
        logging.info("Received revocation via SCMP:\n%s",
                     rev_info.short_desc())
        self._process_revocation(rev_info)

    def _handle_revocation(self, rev_info, meta):
        logging.info("Received revocation via TCP/UDP:\n%s",
                     rev_info.short_desc())
        if not self._validate_revocation(rev_info):
            return
        self._process_revocation(rev_info)

    def handle_rev_objs(self):
        with self.local_rev_cache_lock:
            for rev_info in self.local_rev_cache.values():
                self._remove_revoked_pcbs(rev_info)

    def _process_revocation(self, rev_info):
        """
        Removes PCBs containing a revoked interface and sends the revocation
        to the local PS.

        :param rev_info: The RevocationInfo object
        :type rev_info: RevocationInfo
        """
        assert isinstance(rev_info, RevocationInfo)
        if_id = rev_info.p.ifID
        if not if_id:
            logging.error("Trying to revoke IF with ID 0.")
            return

        with self.local_rev_cache_lock:
            self.local_rev_cache[rev_info] = rev_info.copy()

        logging.info("Storing revocation in ZK.")
        rev_token = rev_info.copy().pack()
        entry_name = "%s:%s" % (hash(rev_token), time.time())
        try:
            self.revobjs_cache.store(entry_name, rev_token)
        except ZkNoConnection as exc:
            logging.error("Unable to store revocation in shared cache "
                          "(no ZK connection): %s" % exc)
        self._remove_revoked_pcbs(rev_info)

    @abstractmethod
    def _remove_revoked_pcbs(self, rev_info):
        """
        Removes the PCBs containing the revoked interface.

        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        """
        raise NotImplementedError

    def _pcb_list_to_remove(self, candidates, rev_info):
        """
        Calculates the list of PCBs to remove.
        Called by _remove_revoked_pcbs.

        :param candidates: Candidate PCBs.
        :type candidates: List
        :param rev_info: The RevocationInfo object.
        :type rev_info: RevocationInfo
        """
        to_remove = []
        processed = set()
        for cand in candidates:
            if cand.id in processed:
                continue
            processed.add(cand.id)
            if not ConnectedHashTree.verify_epoch(rev_info.p.epoch):
                continue

            # If the interface on which we received the PCB is
            # revoked, then the corresponding pcb needs to be removed, if
            # the proof can be verified with the own AS's root for the current
            # epoch and  the if_id of the interface on which pcb was received
            # matches that in the rev_info
            root_verify = ConnectedHashTree.verify(rev_info,
                                                   self._get_ht_root())
            if (self.addr.isd_as == rev_info.isd_as()
                    and cand.pcb.p.ifID == rev_info.p.ifID and root_verify):
                to_remove.append(cand.id)

            for asm in cand.pcb.iter_asms():
                if self._verify_revocation_for_asm(rev_info, asm, False):
                    to_remove.append(cand.id)

        return to_remove

    def _handle_if_timeouts(self):
        """
        Periodically checks each interface state and issues an if revocation, if
        no keep-alive message was received for IFID_TOUT.
        """
        if_id_last_revoked = defaultdict(int)
        while self.run_flag.is_set():
            start_time = time.time()
            with self.ifid_state_lock:
                for (if_id, if_state) in self.ifid_state.items():
                    cur_epoch = ConnectedHashTree.get_current_epoch()
                    # Check if interface has timed-out.
                    if ((if_state.is_expired() or if_state.is_revoked())
                            and (if_id_last_revoked[if_id] != cur_epoch)):
                        if_id_last_revoked[if_id] = cur_epoch
                        if not if_state.is_revoked():
                            logging.info("IF %d appears to be down.", if_id)
                        self._issue_revocation(if_id)
                        if_state.revoke_if_expired()
            sleep_interval(start_time, self.IF_TIMEOUT_INTERVAL,
                           "Handle IF timeouts")

    def _handle_ifstate_request(self, req, meta):
        # Only master replies to ifstate requests.
        if not self.zk.have_lock():
            return
        assert isinstance(req, IFStateRequest)
        logging.debug("Received ifstate req:\n%s", req)
        infos = []
        with self.ifid_state_lock:
            if req.p.ifID == IFStateRequest.ALL_INTERFACES:
                ifid_states = self.ifid_state.items()
            elif req.p.ifID in self.ifid_state:
                ifid_states = [(req.p.ifID, self.ifid_state[req.p.ifID])]
            else:
                logging.error(
                    "Received ifstate request from %s for unknown "
                    "interface %s.", meta.get_addr(), req.p.ifID)
                return

            for (ifid, state) in ifid_states:
                # Don't include inactive interfaces in response.
                if state.is_inactive():
                    continue
                info = IFStateInfo.from_values(ifid, state.is_active(),
                                               self._get_ht_proof(ifid))
                infos.append(info)
        if not infos and not self._quiet_startup():
            logging.warning("No IF state info to put in response.")
            return
        payload = IFStatePayload.from_values(infos)
        self.send_meta(payload, meta, (meta.host, meta.port))
Example #27
0
 def __init__(
     self,
     server_id,
     conf_dir,
 ):
     """
     :param str server_id: server identifier.
     :param str conf_dir: configuration directory.
     """
     super().__init__(
         server_id,
         conf_dir,
     )
     self.interface = None
     for edge_router in self.topology.get_all_edge_routers():
         if edge_router.addr == self.addr.host:
             self.interface = edge_router.interface
             break
     assert self.interface is not None
     logging.info("Interface: %s", self.interface.__dict__)
     self.of_gen_key = PBKDF2(self.config.master_as_key, b"Derive OF Key")
     self.sibra_key = PBKDF2(self.config.master_as_key, b"Derive SIBRA Key")
     self.if_states = defaultdict(InterfaceState)
     self.revocations = ExpiringDict(1000, self.FWD_REVOCATION_TIMEOUT)
     self.pre_ext_handlers = {
         SibraExtBase.EXT_TYPE:
         self.handle_sibra,
         TracerouteExt.EXT_TYPE:
         self.handle_traceroute,
         ExtHopByHopType.SCMP:
         self.handle_scmp,
         HORNETPlugin.EXT_TYPE:
         HORNETPlugin(self.id, conf_dir, self.config.master_as_key,
                      self.addr, self.interface).pre_routing
     }
     self.post_ext_handlers = {
         SibraExtBase.EXT_TYPE: False,
         TracerouteExt.EXT_TYPE: False,
         ExtHopByHopType.SCMP: False,
         HORNETPlugin.EXT_TYPE: False
     }
     self.sibra_state = SibraState(
         self.interface.bandwidth, "%s#%s -> %s" %
         (self.addr.isd_as, self.interface.if_id, self.interface.isd_as))
     self.CTRL_PLD_CLASS_MAP = {
         PayloadClass.PCB: {
             PCBType.SEGMENT: self.process_pcb
         },
         PayloadClass.IFID: {
             IFIDType.PAYLOAD: self.process_ifid_request
         },
         PayloadClass.CERT:
         defaultdict(lambda: self.relay_cert_server_packet),
         PayloadClass.PATH:
         defaultdict(lambda: self.process_path_mgmt_packet),
         PayloadClass.SIBRA: {
             SIBRAPayloadType.EMPTY: self.fwd_sibra_service_pkt
         },
     }
     self.SCMP_PLD_CLASS_MAP = {
         SCMPClass.PATH: {
             SCMPPathClass.REVOKED_IF: self.process_revocation
         },
     }
     self._remote_sock = UDPSocket(
         bind=(str(self.interface.addr), self.interface.udp_port),
         addr_type=AddrType.IPV4,
     )
     self._socks.add(self._remote_sock, self.handle_recv)
     logging.info("IP %s:%d", self.interface.addr, self.interface.udp_port)