def __init__(self, tid, raw_socket, client_ip, user, start_stats=True): """Reads topology with the specified id from the database. A DoesNotExist exception (Topology, IPAssignment, or IPBlockAllocation) is raised if this fails. Note that this should only be invoked from the database thread.""" self.raw_socket = raw_socket # stores jobs which need to be done for this topology self.job_queue = DRRQueue(MAX_JOBS_PER_TOPO) # maps clients connected to this topology to the node they are connected to self.clients = {} # a list of packets destined to the first hop pending an ARP translation self.pending_incoming_packets = [] # current ARP translations - this maps a string representing an IP (e.g. # '\x12\x34\x56\x78' for 18.52.86.120 to a tuple of the form # (mac, last_updated, last_request); mac and last_updated can be None self.arp_cache = {} self.t = db.Topology.objects.get(pk=tid) if not self.t.enabled: raise TopologyCreationException('topology %d is disabled' % tid) self.id = tid self.temporary = self.t.temporary # Allocate an IP block and assign IPs if necessary allocate_to_topology(self.t, self.t.ip_block, user) # determine what IP block is allocated to this topology ipba = db.IPBlockAllocation.objects.get(topology=self.t) self.ip_block = (struct.unpack('>I',inet_aton(ipba.start_addr))[0], ipba.mask) # determine what IPs may interact with this topology tus = db.TopologySourceIPFilter.objects.filter(topology=self.t) if len(tus) > 0: self.permitted_source_prefixes = [tu.subnet_mask_str() for tu in tus] else: self.permitted_source_prefixes = ['0.0.0.0/0'] # unrestricted # Salt for MAC address generation: ensures a topology which reuses # shared IPs still gets unique MAC addresses so that ARP requests really # intended for other topologies don't end up on every copy of the topo. self.mac_salt = ''.join([hashlib.md5(psp).digest() for psp in self.permitted_source_prefixes]) # read in this topology's nodes db_nodes = db.Node.objects.filter(template=self.t.template) self.gateway = None self.nodes = [self.__make_node(dn, raw_socket) for dn in db_nodes] # remember the DB to simulator object mapping nodes_db_to_sim = {} for i in range(len(db_nodes)): dn = db_nodes[i] sn = self.nodes[i] nodes_db_to_sim[dn] = sn # read in this topology's ports interfaces_db_to_sim = {} db_ports = db.Port.objects.filter(node__template=self.t.template) for dp in db_ports: sn = nodes_db_to_sim[dp.node] # TODO: we're hitting the DB a lot here; could optimize a bit mac, ip, mask = self.__get_addr_assignments_for_node(self.t, sn, dp) intf = sn.add_interface(dp.name, mac, ip, mask) interfaces_db_to_sim[dp] = intf # read in this topology's links links = db.Link.objects.filter(port1__node__template=self.t.template) for db_link in links: intf1 = interfaces_db_to_sim[db_link.port1] intf2 = interfaces_db_to_sim[db_link.port2] Link(intf1, intf2, db_link.lossiness) # get the interface to the first hop (if a first hop exists) self.gw_intf_to_first_hop = None if len(self.gateway.interfaces) > 0: intf = self.gateway.interfaces[0] if intf.link: self.gw_intf_to_first_hop = intf if start_stats: self.stats = db.UsageStats() self.stats.init(self.t, client_ip, user) self.stats.save() logging.info('Topology instantiated:\n%s' % self.str_all(include_clients=False))
def __init__(self, tid, raw_socket, client_ip, user, start_stats=True): """Reads topology with the specified id from the database. A DoesNotExist exception (Topology, IPAssignment, or IPBlockAllocation) is raised if this fails.""" self.raw_socket = raw_socket # stores jobs which need to be done for this topology self.job_queue = DRRQueue(MAX_JOBS_PER_TOPO) # maps clients connected to this topology to the node they are connected to self.clients = {} # a list of packets destined to the first hop pending an ARP translation self.pending_incoming_packets = [] # last time an ARP translation was completed / requested self.last_arp_translation = 0 self.last_arp_translation_request = 0 # current ARP translation self.arp_translation = None t = db.Topology.objects.get(pk=tid) if not t.enabled: raise TopologyCreationException('topology %d is disabled' % tid) self.id = tid self.temporary = t.temporary # determine what IP block is allocated to this topology ipba = db.IPBlockAllocation.objects.get(topology=t) self.ip_block = (struct.unpack('>I',inet_aton(ipba.start_addr))[0], ipba.mask) # determine who may connect to nodes in this topology if t.public: # anyone may use it self.permitted_clients = None else: tufs = db.TopologyUserFilter.objects.filter(topology=t) self.permitted_clients = [tuf.user for tuf in tufs] self.permitted_clients.append(t.owner) # determine what IPs may interact with this topology tus = db.TopologySourceIPFilter.objects.filter(topology=t) if len(tus) > 0: self.permitted_source_prefixes = [tu.subnet_mask_str() for tu in tus] else: self.permitted_source_prefixes = ['0.0.0.0/0'] # unrestricted # Salt for MAC address generation: ensures a topology which reuses # shared IPs still gets unique MAC addresses so that ARP requests really # intended for other topologies don't end up on every copy of the topo. self.mac_salt = ''.join([hashlib.md5(psp).digest() for psp in self.permitted_source_prefixes]) # read in this topology's nodes db_nodes = db.Node.objects.filter(template=t.template) self.gateway = None self.nodes = [self.__make_node(dn, raw_socket) for dn in db_nodes] # remember the DB to simulator object mapping nodes_db_to_sim = {} for i in range(len(db_nodes)): dn = db_nodes[i] sn = self.nodes[i] nodes_db_to_sim[dn] = sn # read in this topology's ports interfaces_db_to_sim = {} db_ports = db.Port.objects.filter(node__template=t.template) for dp in db_ports: sn = nodes_db_to_sim[dp.node] # TODO: we're hitting the DB a lot here; could optimize a bit mac, ip, mask = self.__get_addr_assignments_for_node(t, sn, dp) intf = sn.add_interface(dp.name, mac, ip, mask) interfaces_db_to_sim[dp] = intf # read in this topology's links links = db.Link.objects.filter(port1__node__template=t.template) for db_link in links: intf1 = interfaces_db_to_sim[db_link.port1] intf2 = interfaces_db_to_sim[db_link.port2] Link(intf1, intf2, db_link.lossiness) # get the interface to the first hop (if a first hop exists) self.gw_intf_to_first_hop = None if len(self.gateway.interfaces) > 0: intf = self.gateway.interfaces[0] if intf.link: self.gw_intf_to_first_hop = intf if start_stats: self.stats = db.UsageStats() self.stats.init(t, client_ip, user) self.stats.save() logging.info('Topology instantiated:\n%s' % self.str_all(include_clients=False))
class Topology(): """A topology to simulate.""" def __init__(self, tid, raw_socket, client_ip, user, start_stats=True): """Reads topology with the specified id from the database. A DoesNotExist exception (Topology, IPAssignment, or IPBlockAllocation) is raised if this fails. Note that this should only be invoked from the database thread.""" self.raw_socket = raw_socket # stores jobs which need to be done for this topology self.job_queue = DRRQueue(MAX_JOBS_PER_TOPO) # maps clients connected to this topology to the node they are connected to self.clients = {} # a list of packets destined to the first hop pending an ARP translation self.pending_incoming_packets = [] # current ARP translations - this maps a string representing an IP (e.g. # '\x12\x34\x56\x78' for 18.52.86.120 to a tuple of the form # (mac, last_updated, last_request); mac and last_updated can be None self.arp_cache = {} self.t = db.Topology.objects.get(pk=tid) if not self.t.enabled: raise TopologyCreationException('topology %d is disabled' % tid) self.id = tid self.temporary = self.t.temporary # Allocate an IP block and assign IPs if necessary allocate_to_topology(self.t, self.t.ip_block, user) # determine what IP block is allocated to this topology ipba = db.IPBlockAllocation.objects.get(topology=self.t) self.ip_block = (struct.unpack('>I',inet_aton(ipba.start_addr))[0], ipba.mask) # determine what IPs may interact with this topology tus = db.TopologySourceIPFilter.objects.filter(topology=self.t) if len(tus) > 0: self.permitted_source_prefixes = [tu.subnet_mask_str() for tu in tus] else: self.permitted_source_prefixes = ['0.0.0.0/0'] # unrestricted # Salt for MAC address generation: ensures a topology which reuses # shared IPs still gets unique MAC addresses so that ARP requests really # intended for other topologies don't end up on every copy of the topo. self.mac_salt = ''.join([hashlib.md5(psp).digest() for psp in self.permitted_source_prefixes]) # read in this topology's nodes db_nodes = db.Node.objects.filter(template=self.t.template) self.gateway = None self.nodes = [self.__make_node(dn, raw_socket) for dn in db_nodes] # remember the DB to simulator object mapping nodes_db_to_sim = {} for i in range(len(db_nodes)): dn = db_nodes[i] sn = self.nodes[i] nodes_db_to_sim[dn] = sn # read in this topology's ports interfaces_db_to_sim = {} db_ports = db.Port.objects.filter(node__template=self.t.template) for dp in db_ports: sn = nodes_db_to_sim[dp.node] # TODO: we're hitting the DB a lot here; could optimize a bit mac, ip, mask = self.__get_addr_assignments_for_node(self.t, sn, dp) intf = sn.add_interface(dp.name, mac, ip, mask) interfaces_db_to_sim[dp] = intf # read in this topology's links links = db.Link.objects.filter(port1__node__template=self.t.template) for db_link in links: intf1 = interfaces_db_to_sim[db_link.port1] intf2 = interfaces_db_to_sim[db_link.port2] Link(intf1, intf2, db_link.lossiness) # get the interface to the first hop (if a first hop exists) self.gw_intf_to_first_hop = None if len(self.gateway.interfaces) > 0: intf = self.gateway.interfaces[0] if intf.link: self.gw_intf_to_first_hop = intf if start_stats: self.stats = db.UsageStats() self.stats.init(self.t, client_ip, user) self.stats.save() logging.info('Topology instantiated:\n%s' % self.str_all(include_clients=False)) def __get_addr_assignments_for_node(self, t, sn, dp): if sn.get_type_str() == 'Gateway': # TODO: get the appropriate simulator object (assuming there's only one for now) sim = db.Simulator.objects.all()[0] ip = inet_aton(sim.gatewayIP) mac = ''.join([struct.pack('>B', int(b, 16)) for b in sim.gatewayMAC.split(':')]) return (mac, ip, '\x00\x00\x00\x00') else: try: ipa = db.IPAssignment.objects.get(topology=t, port=dp) except db.IPAssignment.DoesNotExist: ipa = None try: mac = db.MACAssignment.objects.get(topology=t, port=dp).get_mac() except db.MACAssignment.DoesNotExist: if ipa is not None: mac = ipa.get_mac(self.mac_salt) else: mac = os.urandom(6); if ipa is not None: return (mac, ipa.get_ip(), ipa.get_mask()) else: return (mac, None, None) def connect_client(self, client_conn, client_user, requested_name): """Called when a user tries to connect to a node in this topology. Returns True if the requested node exists and the client was able to connect to it. Otherwise it returns an error message.""" if not self.can_connect(client_user): return ConnectionReturn('%s is not authorized to use this topology' % client_user) for n in self.nodes: if n.name == requested_name: self.clients[client_conn] = n ret = n.connect(client_conn) if ret.is_success(): client_conn.send(VNSHardwareInfo(n.interfaces)) fmt = 'client (%s) has connected to node %s on topology %d' logging.info(fmt % (client_conn, n, self.id)) return ret return ConnectionReturn('there is no node named %s' % requested_name) def client_disconnected(self, client_conn): n = self.clients.pop(client_conn, None) if n: n.disconnect(client_conn) def get_clients(self): """Returns a list of clients connected to this Topology.""" return self.clients.keys() def has_gateway(self): """Returns True if this topology has a gateway.""" return self.gateway is not None def is_temporary(self): """Returns True if this topology is only temporary.""" return self.temporary def get_my_ip_addrs(self): """Returns a list of IP addresses (as byte-strings) which belong to nodes (except the gateway) in this topology.""" addrs = [] for node in self.nodes: if node is not self.gateway: for intf in node.interfaces: if intf.ip: addrs.append(intf.ip) return addrs def get_my_ip_block(self): """Returns a 2-tuple containing the subnet and associated mask which contains all IPs assigned to this topology. The subnet is expressed as a 4B NBO integer.""" return self.ip_block def get_all_ip_addrs_in_my_ip_block(self): """Returns a list of NBO byte-strings representing the IPs allocated to this topology.""" dst_block_start_ip, dst_block_mask = self.get_my_ip_block() return [struct.pack('>I',dst_block_start_ip+i) for i in xrange(2**(32-dst_block_mask))] def get_my_mac_addrs(self): """Returns a list of Ethernet addresses (as byte-strings) which belong to nodes (except the gateway) in this topology.""" addrs = [] for node in self.nodes: if node is not self.gateway: for intf in node.interfaces: addrs.append(intf.mac) return addrs def get_id(self): """Returns this topology's unique ID number.""" return self.id def get_source_filters(self): """Returns a list of IP prefixes which should be routed to this topology. This list will always contain at least one prefix.""" return self.permitted_source_prefixes def get_stats(self): """Returns the UsageStats object maintained by this Topology instance.""" return self.stats def handle_packet_from_client(self, conn, pkt_msg): """Sends the specified message out the specified port on the node controlled by conn. If conn does not control a node, then a KeyError is raised. If conn's node does not have an interface with the specified name then an error message is returned. Otherwise, True is returned.""" departure_intf_name = pkt_msg.intf_name n = self.clients[conn] for intf in n.interfaces: if intf.name == departure_intf_name: logging.debug('%s: client sending packet from %s out %s: %s' % (self, n.name, intf.name, pktstr(pkt_msg.ethernet_frame))) self.stats.note_pkt_from_client(len(pkt_msg.ethernet_frame)) n.send_packet(intf, pkt_msg.ethernet_frame) return True # failed to find the specified interface fmt = 'bad packet request: invalid interface: %s' return fmt % ((n.name, departure_intf_name),) def create_job_for_incoming_packet(self, packet, rewrite_dst_mac): """Enqueues a job for handling this packet with handle_incoming_packet().""" try: self.job_queue.put_nowait(lambda : self.handle_incoming_packet(packet, rewrite_dst_mac)) except Queue.Full: logging.debug("Queue full for topology %s, dropping incoming packet: %s" % (str(self), pktstr(packet))) def handle_incoming_packet(self, packet, rewrite_dst_mac): """Forwards packet to the node connected to the gateway. If rewrite_dst_mac is True then the destination mac is set to that of the first simulated node attached to the gateway.""" gw_intf = self.gw_intf_to_first_hop if gw_intf: self.stats.note_pkt_to_topo(len(packet)) if rewrite_dst_mac: # Try to get the next hop IP for this packet; if we can't find # one, log and drop it try: ip = self.get_next_hop(packet) except ValueError: # Couldn't find the next IP for this packet logging.debug("Dropped packet: unable to resolve to IP") return # Try to get the MAC for this IP from the ARP cache and send the # packet; if there's no entry in the cache, send an arp request new_dst_mac = self.get_arp_cache_entry(ip) if new_dst_mac is None: self.need_arp_translation_for_pkt(packet, ip) else: gw_intf.link.send_to_other(gw_intf, new_dst_mac + packet[6:]) else: gw_intf.link.send_to_other(gw_intf, packet) def need_arp_translation_for_pkt(self, ethernet_frame, dst_ip): """Delays forwarding a packet to the node connected to the gateway until it replies to an ARP request.""" if type(dst_ip) != str or len(dst_ip) != 4: raise ValueError("dst_ip must be a string of length 4") if len(self.pending_incoming_packets) < 10: self.pending_incoming_packets.append((ethernet_frame, dst_ip)) # otherwise: drop new packets if the psuedo-queue is full if not self.is_ok_to_send_arp_request(dst_ip): return # we already sent an ARP request recently, so be patient! self.update_last_arp_request_time(dst_ip, time.time()) gw_intf = self.gw_intf_to_first_hop dst_mac = '\xFF\xFF\xFF\xFF\xFF\xFF' # broadcast src_mac = gw_intf.mac eth_type = '\x08\x06' eth_hdr = dst_mac + src_mac + eth_type src_ip = gw_intf.ip # hdr: HW=Eth, Proto=IP, HWLen=6, ProtoLen=4, Type=Request arp_hdr = '\x00\x01\x08\x00\x06\x04\x00\x01' arp_request = eth_hdr + arp_hdr + src_mac + src_ip + dst_mac + dst_ip gw_intf.link.send_to_other(gw_intf, arp_request) def update_last_arp_request_time(self, ip, time): """Updates the time of the last request to an IP in the ARP cache.""" try: (mac, last_update, _) = self.arp_cache[ip] except KeyError: mac = None last_update = None self.arp_cache[ip] = (mac, last_update, time) def get_next_hop(self, ethernet_frame): """Finds the IP address of the next hop in the topology for an incoming packet. @param ethernet_frame The ethernet frame which is being forwarded""" # See if the interface the gateway is connected to has an IP gw_intf = self.gw_intf_to_first_hop dst_ip = gw_intf.link.get_other(gw_intf).ip if dst_ip is not None: return dst_ip # If that interface doesn't have an IP, see if we can deduce the IP from # the packet if len(ethernet_frame) >= 34: (ethertype,) = struct.unpack_from("!H", ethernet_frame, 12) if ethertype == 0x0800: # It's an IP packet, so we can read off the destination IP dst_ip = ethernet_frame[30:34] return dst_ip # Otherwise, we give up raise ValueError("No IP address found") def update_arp_translation(self, ip, mac): """Updates the ARP translation to the first hop and sends out any pending packets.""" try: (_, _, last_request) = self.arp_cache[ip] except KeyError: last_request = 0 self.arp_cache[ip] = (mac, time.time(), last_request) gw_intf = self.gw_intf_to_first_hop if gw_intf: for (packet, pkt_ip) in self.pending_incoming_packets: if pkt_ip == ip: new_pkt = mac + packet[6:] gw_intf.link.send_to_other(gw_intf, new_pkt) self.pending_incoming_packets.remove((packet, pkt_ip)) def get_node_and_intf_with_link(self, node_name, intf_name): """Returns a 2-tuple containing the named node and interface if they exist and there is a link from it. Case-insensitive. Otherwise TIBadNodeOrPort is raised.""" node_name = node_name.lower() intf_name = intf_name.lower() for n in self.nodes: if n.name.lower() == node_name: for intf in n.interfaces: if intf.name.lower() == intf_name: if intf.link: return (n, intf) else: raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.MISSING_LINK) raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.BAD_INTF) raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.BAD_NODE) def send_packet_from_node(self, node_name, intf_name, ethernet_frame): """Sends a packet from the request node's specified interface. If the node or port is invalid, TIBadNodeOrPort is raised.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) self.stats.note_pkt_to_topo(len(ethernet_frame)) intf.link.send_to_other(intf, ethernet_frame) def send_ping_from_node(self, node_name, intf_name, dst_ip): """Sends a ping from the request node's specified interface. If the node or port is invalid, TIBadNodeOrPort is raised.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) mac_dst = intf.link.get_other(intf).mac mac_src = intf.mac mac_type = '\x08\x00' ethernet_hdr = mac_dst + mac_src + mac_type src_ip = intf.ip ip_hdr = Packet.cksum_ip_hdr('\x45\x00\x00\x54\x00\x00\x40\x00\x40\x01\x00\x00' + src_ip + dst_ip) # 56 bytes of data needed since IP tlen field set to expect a 64B payload (8B ICMP hdr + 56B data) icmp_data = 'This echo request sent through VNS for a topo interactor' icmp = Packet.cksum_icmp_pkt('\x08\x00\x00\x00\x00\x00\x00\x01' + icmp_data) ethernet_frame = ethernet_hdr + ip_hdr + icmp intf.link.send_to_other(intf, ethernet_frame) def modify_link(self, node_name, intf_name, new_lossiness): """Sets the link attached to the specified port to the new lossiness value. Returns a string describing what happened. TIBadNodeOrPort is raised the node or interface specified is invalid.""" if new_lossiness<0.0 or new_lossiness>1.0: return "Error: lossiness requested is not in [0,1]" _, i = self.get_node_and_intf_with_link(node_name, intf_name) old_lossiness_txt = i.link.str_lossiness() i.link.lossiness = new_lossiness new_lossiness_txt = i.link.str_lossiness() if old_lossiness_txt == new_lossiness_txt: return 'No link modification required (was already %s)' % old_lossiness_txt else: return 'Link modified from "%s" to "%s"' % (old_lossiness_txt, new_lossiness_txt) def tap_node(self, node_name, intf_name, tap, tap_config): """Sets the state of a tap on a given node. If there was a tap, then it is replaced if a new one is specified. A string is returned which describes the action taken. TIBadNodeOrPort is raised if node or intf name is invalid.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) if not tap: if intf.tap: intf.tap = None return "tap on %s:%s has been disabled" % (node_name, intf_name) else: return "There was no tap on %s:%s" % (node_name, intf_name) else: if intf.tap: msg = "tap on %s:%s has been replaced with the new tap" % (node_name, intf_name) else: msg = "tap on %s:%s has been installed" % (node_name, intf_name) intf.tap = tap_config return msg def clear_taps(self, ti_conn): """Clears all taps associated with the specified TI connection.""" for n in self.nodes: for intf in n.interfaces: if intf.tap and intf.tap.ti_conn==ti_conn: intf.tap = None def send_packet_to_gateway(self, ethernet_frame): """Sends an Ethernet frame to the gateway; the destination MAC address is set appropriately.""" if self.gw_intf_to_first_hop and self.raw_socket: mac_dst = self.gw_intf_to_first_hop.mac new_eth_frame = mac_dst + ethernet_frame[6:] self.raw_socket.send(new_eth_frame) def is_active(self): """Returns true if any clients are connected.""" return len(self.clients) > 0 def get_arp_cache_entry(self, ip): """Returns the MAC address associated with an IP in the ARP cache, or None if no such MAC is found.""" try: (mac, last_updated, last_request) = self.arp_cache[ip] except KeyError: return None if mac is None: return None if last_updated is None: return None if last_updated + ARP_CACHE_TIMEOUT < time.time(): del self.arp_cache[ip] return None return mac def is_ok_to_send_arp_request(self, ip): """Returns True if a reasonable amount of time has passed since the last ARP request was sent.""" try: (_, _, last_request) = self.arp_cache[ip] except KeyError: return True return time.time() - last_request >= ARP_REQUEST_TIMEOUT def __make_node(self, dn, raw_socket): """Converts the given database node into a simulator node object.""" # TODO: need to distinguish between nodes THIS simulator simulates, # versus nodes which ANOTHER simulator is responsible for. Do # this with a RemotelySimulatedNode class which handles received # packets by forwarding them to the appropriate simulator. topo = self if dn.type == db.Node.VIRTUAL_NODE_ID: return VirtualNode(topo, dn.name) elif dn.type == db.Node.BLACK_HOLE_ID: return BlackHole(topo, dn.name) elif dn.type == db.Node.HUB_ID: return Hub(topo, dn.name) elif dn.type == db.Node.WEB_SERVER_ID: path = WEB_SERVER_ROOT_WWW + dn.webserver.path_to_serve.get_ascii_path() return WebServer(topo, dn.name, path) elif dn.type == db.Node.GATEWAY_ID: if self.gateway is not None: err = 'only one gateway per topology is allowed' else: self.gateway = Gateway(topo, dn.name, raw_socket) return self.gateway else: err = 'unknown node type: %d' % dn.type logging.critical(err) raise db.Topology.DoesNotExist(err) def __str__(self): return 'Topology %d' % self.id def str_all(self, include_clients=True): """Returns a complete string representation of this topology.""" str_hdr = self.__str__() if not include_clients: str_clients = '' elif self.clients: str_clients = 'Clients: %s\n ' % ','.join([str(c) for c in self.clients]) else: str_clients = 'Clients: none connected\n ' str_psp = 'Source IP Prefixes: %s' % ','.join(self.permitted_source_prefixes) str_nodes = 'Nodes:\n ' + '\n '.join([n.str_all() for n in self.nodes]) return '%s:\n %s%s\n %s' % (str_hdr, str_clients, str_psp, str_nodes) def can_connect(self, username): """Returns True if the user with this username can connect to the topology, False otherwise.""" try: user = User.objects.get(username=username) except User.DoesNotExist: return False return permissions.allowed_topology_access_use(user, self.t)
class Topology(): """A topology to simulate.""" def __init__(self, tid, raw_socket, client_ip, user, start_stats=True): """Reads topology with the specified id from the database. A DoesNotExist exception (Topology, IPAssignment, or IPBlockAllocation) is raised if this fails.""" self.raw_socket = raw_socket # stores jobs which need to be done for this topology self.job_queue = DRRQueue(MAX_JOBS_PER_TOPO) # maps clients connected to this topology to the node they are connected to self.clients = {} # a list of packets destined to the first hop pending an ARP translation self.pending_incoming_packets = [] # last time an ARP translation was completed / requested self.last_arp_translation = 0 self.last_arp_translation_request = 0 # current ARP translation self.arp_translation = None t = db.Topology.objects.get(pk=tid) if not t.enabled: raise TopologyCreationException('topology %d is disabled' % tid) self.id = tid self.temporary = t.temporary # determine what IP block is allocated to this topology ipba = db.IPBlockAllocation.objects.get(topology=t) self.ip_block = (struct.unpack('>I',inet_aton(ipba.start_addr))[0], ipba.mask) # determine who may connect to nodes in this topology if t.public: # anyone may use it self.permitted_clients = None else: tufs = db.TopologyUserFilter.objects.filter(topology=t) self.permitted_clients = [tuf.user for tuf in tufs] self.permitted_clients.append(t.owner) # determine what IPs may interact with this topology tus = db.TopologySourceIPFilter.objects.filter(topology=t) if len(tus) > 0: self.permitted_source_prefixes = [tu.subnet_mask_str() for tu in tus] else: self.permitted_source_prefixes = ['0.0.0.0/0'] # unrestricted # Salt for MAC address generation: ensures a topology which reuses # shared IPs still gets unique MAC addresses so that ARP requests really # intended for other topologies don't end up on every copy of the topo. self.mac_salt = ''.join([hashlib.md5(psp).digest() for psp in self.permitted_source_prefixes]) # read in this topology's nodes db_nodes = db.Node.objects.filter(template=t.template) self.gateway = None self.nodes = [self.__make_node(dn, raw_socket) for dn in db_nodes] # remember the DB to simulator object mapping nodes_db_to_sim = {} for i in range(len(db_nodes)): dn = db_nodes[i] sn = self.nodes[i] nodes_db_to_sim[dn] = sn # read in this topology's ports interfaces_db_to_sim = {} db_ports = db.Port.objects.filter(node__template=t.template) for dp in db_ports: sn = nodes_db_to_sim[dp.node] # TODO: we're hitting the DB a lot here; could optimize a bit mac, ip, mask = self.__get_addr_assignments_for_node(t, sn, dp) intf = sn.add_interface(dp.name, mac, ip, mask) interfaces_db_to_sim[dp] = intf # read in this topology's links links = db.Link.objects.filter(port1__node__template=t.template) for db_link in links: intf1 = interfaces_db_to_sim[db_link.port1] intf2 = interfaces_db_to_sim[db_link.port2] Link(intf1, intf2, db_link.lossiness) # get the interface to the first hop (if a first hop exists) self.gw_intf_to_first_hop = None if len(self.gateway.interfaces) > 0: intf = self.gateway.interfaces[0] if intf.link: self.gw_intf_to_first_hop = intf if start_stats: self.stats = db.UsageStats() self.stats.init(t, client_ip, user) self.stats.save() logging.info('Topology instantiated:\n%s' % self.str_all(include_clients=False)) def __get_addr_assignments_for_node(self, t, sn, dp): if sn.get_type_str() == 'Gateway': # TODO: get the appropriate simulator object (assuming there's only one for now) sim = db.Simulator.objects.all()[0] ip = inet_aton(sim.gatewayIP) mac = ''.join([struct.pack('>B', int(b, 16)) for b in sim.gatewayMAC.split(':')]) return (mac, ip, '\x00\x00\x00\x00') else: ipa = db.IPAssignment.objects.get(topology=t, port=dp) try: mac = db.MACAssignment.objects.get(topology=t, port=dp).get_mac() except db.MACAssignment.DoesNotExist: mac = ipa.get_mac(self.mac_salt) return (mac, ipa.get_ip(), ipa.get_mask()) def connect_client(self, client_conn, client_user, requested_name): """Called when a user tries to connect to a node in this topology. Returns True if the requested node exists and the client was able to connect to it. Otherwise it returns an error message.""" if self.permitted_clients is not None: # otherwise anyone can use it if client_user not in self.permitted_clients: return ConnectionReturn('%s is not authorized to use this topology' % client_user) for n in self.nodes: if n.name == requested_name: self.clients[client_conn] = n ret = n.connect(client_conn) if ret.is_success(): client_conn.send(VNSHardwareInfo(n.interfaces)) fmt = 'client (%s) has connected to node %s on topology %d' logging.info(fmt % (client_conn, n, self.id)) return ret return ConnectionReturn('there is no node named %s' % requested_name) def client_disconnected(self, client_conn): n = self.clients.pop(client_conn, None) if n: n.disconnect(client_conn) def get_clients(self): """Returns a list of clients connected to this Topology.""" return self.clients.keys() def has_gateway(self): """Returns True if this topology has a gateway.""" return self.gateway is not None def is_temporary(self): """Returns True if this topology is only temporary.""" return self.temporary def get_my_ip_addrs(self): """Returns a list of IP addresses (as byte-strings) which belong to nodes (except the gateway) in this topology.""" addrs = [] for node in self.nodes: if node is not self.gateway: for intf in node.interfaces: addrs.append(intf.ip) return addrs def get_my_ip_block(self): """Returns a 2-tuple containing the subnet and associated mask which contains all IPs assigned to this topology. The subnet is expressed as a 4B NBO integer.""" return self.ip_block def get_all_ip_addrs_in_my_ip_block(self): """Returns a list of NBO byte-strings representing the IPs allocated to this topology.""" dst_block_start_ip, dst_block_mask = self.get_my_ip_block() return [struct.pack('>I',dst_block_start_ip+i) for i in xrange(2**(32-dst_block_mask))] def get_my_mac_addrs(self): """Returns a list of Ethernet addresses (as byte-strings) which belong to nodes (except the gateway) in this topology.""" addrs = [] for node in self.nodes: if node is not self.gateway: for intf in node.interfaces: addrs.append(intf.mac) return addrs def get_id(self): """Returns this topology's unique ID number.""" return self.id def get_source_filters(self): """Returns a list of IP prefixes which should be routed to this topology. This list will always contain at least one prefix.""" return self.permitted_source_prefixes def get_stats(self): """Returns the UsageStats object maintained by this Topology instance.""" return self.stats def handle_packet_from_client(self, conn, pkt_msg): """Sends the specified message out the specified port on the node controlled by conn. If conn does not control a node, then a KeyError is raised. If conn's node does not have an interface with the specified name then an error message is returned. Otherwise, True is returned.""" departure_intf_name = pkt_msg.intf_name n = self.clients[conn] for intf in n.interfaces: if intf.name == departure_intf_name: logging.debug('%s: client sending packet from %s out %s: %s' % (self, n.name, intf.name, pktstr(pkt_msg.ethernet_frame))) self.stats.note_pkt_from_client(len(pkt_msg.ethernet_frame)) n.send_packet(intf, pkt_msg.ethernet_frame) return True # failed to find the specified interface fmt = 'bad packet request: invalid interface: %s' return fmt % (n.name, departure_intf_name) def create_job_for_incoming_packet(self, packet, rewrite_dst_mac): """Enqueues a job for handling this packet with handle_incoming_packet().""" try: self.job_queue.put_nowait(lambda : self.handle_incoming_packet(packet, rewrite_dst_mac)) except Queue.Full: logging.debug("Queue full for topology %s, dropping incoming packet: %s" % (str(self), pktstr(packet))) def handle_incoming_packet(self, packet, rewrite_dst_mac): """Forwards packet to the node connected to the gateway. If rewrite_dst_mac is True then the destination mac is set to that of the first simulated node attached to the gateway.""" gw_intf = self.gw_intf_to_first_hop if gw_intf: self.stats.note_pkt_to_topo(len(packet)) if rewrite_dst_mac: if self.is_arp_cache_valid(): new_dst_mac = self.arp_translation gw_intf.link.send_to_other(gw_intf, new_dst_mac + packet[6:]) else: self.need_arp_translation_for_pkt(packet) else: gw_intf.link.send_to_other(gw_intf, packet) def need_arp_translation_for_pkt(self, ethernet_frame): """Delays forwarding a packet to the node connected to the gateway until it replies to an ARP request.""" if len(self.pending_incoming_packets) < 10: self.pending_incoming_packets.append(ethernet_frame) # otherwise: drop new packets if the psuedo-queue is full if not self.is_ok_to_send_arp_request(): return # we already sent an ARP request recently, so be patient! else: self.last_arp_translation_request = time.time() gw_intf = self.gw_intf_to_first_hop dst_mac = '\xFF\xFF\xFF\xFF\xFF\xFF' # broadcast src_mac = gw_intf.mac eth_type = '\x08\x06' eth_hdr = dst_mac + src_mac + eth_type dst_ip = gw_intf.link.get_other(gw_intf).ip src_ip = gw_intf.ip # hdr: HW=Eth, Proto=IP, HWLen=6, ProtoLen=4, Type=Request arp_hdr = '\x00\x01\x08\x00\x06\x04\x00\x01' arp_request = eth_hdr + arp_hdr + src_mac + src_ip + dst_mac + dst_ip gw_intf.link.send_to_other(gw_intf, arp_request) def update_arp_translation(self, addr): """Updates the ARP translation to the first hop and sends out any pending packets.""" self.arp_translation = addr self.last_arp_translation = time.time() gw_intf = self.gw_intf_to_first_hop if gw_intf: for packet in self.pending_incoming_packets: new_pkt = self.arp_translation + packet[6:] gw_intf.link.send_to_other(gw_intf, new_pkt) self.pending_incoming_packets = [] # clear the list def get_node_and_intf_with_link(self, node_name, intf_name): """Returns a 2-tuple containing the named node and interface if they exist and there is a link from it. Case-insensitive. Otherwise TIBadNodeOrPort is raised.""" node_name = node_name.lower() intf_name = intf_name.lower() for n in self.nodes: if n.name.lower() == node_name: for intf in n.interfaces: if intf.name.lower() == intf_name: if intf.link: return (n, intf) else: raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.MISSING_LINK) raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.BAD_INTF) raise TIBadNodeOrPort(node_name, intf_name, TIBadNodeOrPort.BAD_NODE) def send_packet_from_node(self, node_name, intf_name, ethernet_frame): """Sends a packet from the request node's specified interface. If the node or port is invalid, TIBadNodeOrPort is raised.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) self.stats.note_pkt_to_topo(len(ethernet_frame)) intf.link.send_to_other(intf, ethernet_frame) def send_ping_from_node(self, node_name, intf_name, dst_ip): """Sends a ping from the request node's specified interface. If the node or port is invalid, TIBadNodeOrPort is raised.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) mac_dst = intf.link.get_other(intf).mac mac_src = intf.mac mac_type = '\x08\x00' ethernet_hdr = mac_dst + mac_src + mac_type src_ip = intf.ip ip_hdr = Packet.cksum_ip_hdr('\x45\x00\x00\x54\x00\x00\x40\x00\x40\x01\x00\x00' + src_ip + dst_ip) # 56 bytes of data needed since IP tlen field set to expect a 64B payload (8B ICMP hdr + 56B data) icmp_data = 'This echo request sent through VNS for a topo interactor' icmp = Packet.cksum_icmp_pkt('\x08\x00\x00\x00\x00\x00\x00\x01' + icmp_data) ethernet_frame = ethernet_hdr + ip_hdr + icmp intf.link.send_to_other(intf, ethernet_frame) def modify_link(self, node_name, intf_name, new_lossiness): """Sets the link attached to the specified port to the new lossiness value. Returns a string describing what happened. TIBadNodeOrPort is raised the node or interface specified is invalid.""" if new_lossiness<0.0 or new_lossiness>1.0: return "Error: lossiness requested is not in [0,1]" _, i = self.get_node_and_intf_with_link(node_name, intf_name) old_lossiness_txt = i.link.str_lossiness() i.link.lossiness = new_lossiness new_lossiness_txt = i.link.str_lossiness() if old_lossiness_txt == new_lossiness_txt: return 'No link modification required (was already %s)' % old_lossiness_txt else: return 'Link modified from "%s" to "%s"' % (old_lossiness_txt, new_lossiness_txt) def tap_node(self, node_name, intf_name, tap, tap_config): """Sets the state of a tap on a given node. If there was a tap, then it is replaced if a new one is specified. A string is returned which describes the action taken. TIBadNodeOrPort is raised if node or intf name is invalid.""" _, intf = self.get_node_and_intf_with_link(node_name, intf_name) if not tap: if intf.tap: intf.tap = None return "tap on %s:%s has been disabled" % (node_name, intf_name) else: return "There was no tap on %s:%s" % (node_name, intf_name) else: if intf.tap: msg = "tap on %s:%s has been replaced with the new tap" % (node_name, intf_name) else: msg = "tap on %s:%s has been installed" % (node_name, intf_name) intf.tap = tap_config return msg def clear_taps(self, ti_conn): """Clears all taps associated with the specified TI connection.""" for n in self.nodes: for intf in n.interfaces: if intf.tap and intf.tap.ti_conn==ti_conn: intf.tap = None def send_packet_to_gateway(self, ethernet_frame): """Sends an Ethernet frame to the gateway; the destination MAC address is set appropriately.""" if self.gw_intf_to_first_hop and self.raw_socket: mac_dst = self.gw_intf_to_first_hop.mac new_eth_frame = mac_dst + ethernet_frame[6:] self.raw_socket.send(new_eth_frame) def is_active(self): """Returns true if any clients are connected.""" return len(self.clients) > 0 def is_arp_cache_valid(self): """Returns True if the ARP cache entry to the first hop is valid.""" return time.time() - self.last_arp_translation <= ARP_CACHE_TIMEOUT def is_ok_to_send_arp_request(self): """Returns True if a reasonable amount of time has passed since the last ARP request was sent.""" return time.time() - self.last_arp_translation_request >= 5 # 5 seconds def __make_node(self, dn, raw_socket): """Converts the given database node into a simulator node object.""" # TODO: need to distinguish between nodes THIS simulator simulates, # versus nodes which ANOTHER simulator is responsible for. Do # this with a RemotelySimulatedNode class which handles received # packets by forwarding them to the appropriate simulator. topo = self if dn.type == db.Node.VIRTUAL_NODE_ID: return VirtualNode(topo, dn.name) elif dn.type == db.Node.BLACK_HOLE_ID: return BlackHole(topo, dn.name) elif dn.type == db.Node.HUB_ID: return Hub(topo, dn.name) elif dn.type == db.Node.WEB_SERVER_ID: path = WEB_SERVER_ROOT_WWW + dn.webserver.path_to_serve.get_ascii_path() return WebServer(topo, dn.name, path) elif dn.type == db.Node.GATEWAY_ID: if self.gateway is not None: err = 'only one gateway per topology is allowed' else: self.gateway = Gateway(topo, dn.name, raw_socket) return self.gateway else: err = 'unknown node type: %d' % dn.type logging.critical(err) raise db.Topology.DoesNotExist(err) def __str__(self): return 'Topology %d' % self.id def str_all(self, include_clients=True): """Returns a complete string representation of this topology.""" str_hdr = self.__str__() if not include_clients: str_clients = '' elif self.clients: str_clients = 'Clients: %s\n ' % ','.join([str(c) for c in self.clients]) else: str_clients = 'Clients: none connected\n ' str_psp = 'Source IP Prefixes: %s' % ','.join(self.permitted_source_prefixes) str_nodes = 'Nodes:\n ' + '\n '.join([n.str_all() for n in self.nodes]) return '%s:\n %s%s\n %s' % (str_hdr, str_clients, str_psp, str_nodes)