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
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)