Example #1
0
File: Topology.py Project: smbz/vns
    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))
Example #2
0
    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))
Example #3
0
File: Topology.py Project: smbz/vns
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)
Example #4
0
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)