コード例 #1
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
コード例 #2
0
ファイル: base.py プロジェクト: jpcsmith/scion-old
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)