diff --git a/examples/multiLink.py b/examples/multiLink.py
new file mode 100755
index 0000000000000000000000000000000000000000..28a67a80baace4cdb25c1e980d8a0801d957b754
--- /dev/null
+++ b/examples/multiLink.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+
+"""
+This is a simple example that demonstrates multiple links
+between nodes.
+"""
+
+from mininet.cli import CLI
+from mininet.log import lg, info
+from mininet.net import Mininet
+from mininet.topo import Topo
+    
+def runMultiLink():
+    
+    topo = simpleMultiLinkTopo( n=2 )
+    net = Mininet( topo=topo )
+    net.start()
+    CLI( net )
+    net.stop()
+
+class simpleMultiLinkTopo( Topo ):
+
+    def __init__( self, n, **kwargs ):
+        Topo.__init__( self, **kwargs )
+
+        h1, h2 = self.addHost( 'h1' ), self.addHost( 'h2' )
+        s1 = self.addSwitch( 's1' )
+        
+        for _ in range( n ):
+            self.addLink( s1, h1 )
+            self.addLink( s1, h2 )
+
+if __name__ == '__main__':
+    lg.setLogLevel( 'info' )
+    runMultiLink()
diff --git a/examples/test/test_multiLink.py b/examples/test/test_multiLink.py
new file mode 100755
index 0000000000000000000000000000000000000000..6b4bca24b075180f38cf53fe294e4f552d2657fe
--- /dev/null
+++ b/examples/test/test_multiLink.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+
+'''
+Test for multiple links between nodes
+validates mininet interfaces against systems interfaces
+'''
+
+import unittest
+import pexpect
+
+class testMultiLink( unittest.TestCase ):
+
+    prompt = 'mininet>'
+
+    def testMultiLink(self):
+        p = pexpect.spawn( 'python -m mininet.examples.multiLink' )
+        p.expect( self.prompt )
+        p.sendline( 'intfs' )
+        p.expect( 's(\d): lo' )
+        intfsOutput = p.before
+        # parse interfaces from mininet intfs, and store them in a list
+        hostToIntfs = intfsOutput.split( '\r\n' )[ 1:3 ]
+        intfList = []
+        for hostToIntf in hostToIntfs:
+            intfList += [ intf for intf in 
+                          hostToIntf.split()[1].split(',') ]
+
+        # get interfaces from system by running ifconfig on every host
+        sysIntfList = []
+        opts = [ 'h(\d)-eth(\d)', self.prompt ]
+        p.expect( self.prompt )
+        
+        p.sendline( 'h1 ifconfig' )
+        while True:
+            p.expect( opts )
+            if p.after == self.prompt:
+                break
+            sysIntfList.append( p.after )
+
+        p.sendline( 'h2 ifconfig' )
+        while True:
+            p.expect( opts )
+            if p.after == self.prompt:
+                break
+            sysIntfList.append( p.after )
+
+        failMsg = ( 'The systems interfaces and mininet interfaces\n'
+                    'are not the same' )
+
+        self.assertEqual( sysIntfList, intfList, msg=failMsg )
+        p.sendline( 'exit' )
+        p.wait()
+
+if __name__ == '__main__':
+        unittest.main()
diff --git a/mininet/net.py b/mininet/net.py
index b74b93002c79cf04c7b35f718d7d674788a47f96..8f89babbab21c1b149a13e2271d833ddf286d67e 100755
--- a/mininet/net.py
+++ b/mininet/net.py
@@ -321,25 +321,36 @@ def items( self ):
         "return (key,value) tuple list for every node in net"
         return zip( self.keys(), self.values() )
 
+    @staticmethod
+    def randMac():
+        "Return a random, non-multicast MAC address"
+        return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff  |
+                            0x020000000000 )
+    
     def addLink( self, node1, node2, port1=None, port2=None,
                  cls=None, **params ):
         """"Add a link from node1 to node2
-            node1: source node
-            node2: dest node
-            port1: source port
-            port2: dest port
+            node1: source node (or name)
+            node2: dest node (or name)
+            port1: source port (optional)
+            port2: dest port (optional)
+            cls: link class (optional)
+            params: additional link params (optional)
             returns: link object"""
-        mac1 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff  | 0x020000000000 )
-        mac2 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff  | 0x020000000000 )
-        defaults = { 'port1': port1,
-                     'port2': port2,
-                     'addr1': mac1,
-                     'addr2': mac2,
-                     'intf': self.intf }
-        defaults.update( params )
-        if not cls:
-            cls = self.link
-        link = cls( node1, node2, **defaults )
+        # Accept node objects or names
+        node1 = node1 if type( node1 ) != str else self[ node1 ]
+        node2 = node2 if type( node2 ) != str else self[ node2 ]
+        options = dict( params )
+        # Port is optional
+        if port1 is not None:
+            options.setdefault( 'port1', port1 )
+        if port2 is not None:
+            options.setdefault( 'port2', port2 )
+        # Set default MAC - this should probably be in Link
+        options.setdefault( 'addr1', self.randMac() )
+        options.setdefault( 'addr2', self.randMac() )
+        cls = self.link if cls is None else cls
+        link = cls( node1, node2, **options )
         self.links.append( link )
         return link
 
@@ -397,12 +408,10 @@ def buildFromTopo( self, topo=None ):
             info( switchName + ' ' )
 
         info( '\n*** Adding links:\n' )
-        for srcName, dstName in topo.links(sort=True):
-            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
-            params = topo.linkInfo( srcName, dstName )
-            srcPort, dstPort = topo.port( srcName, dstName )
-            self.addLink( src, dst, srcPort, dstPort, **params )
-            info( '(%s, %s) ' % ( src.name, dst.name ) )
+        for srcName, dstName, params in topo.links(
+                sort=True, withInfo=True ):
+            self.addLink( **params )
+            info( '(%s, %s) ' % ( srcName, dstName ) )
 
         info( '\n' )
 
diff --git a/mininet/topo.py b/mininet/topo.py
index cc458fd87602e53dc0afab9616d098ef514f4167..af3eac5f55707d83217d85b5101ed27134462fb0 100644
--- a/mininet/topo.py
+++ b/mininet/topo.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-'''@package topo
+"""@package topo
 
 Network topology creation.
 
@@ -9,46 +9,95 @@
 
 A Topo object can be a topology database for NOX, can represent a physical
 setup for testing, and can even be emulated with the Mininet package.
-'''
+"""
 
 from mininet.util import irange, natural, naturalSeq
 
 class MultiGraph( object ):
-    "Utility class to track nodes and edges - replaces networkx.Graph"
+    "Utility class to track nodes and edges - replaces networkx.MultiGraph"
 
     def __init__( self ):
-        self.data = {}
-
-    def add_node( self, node ):
-        "Add node to graph"
-        self.data.setdefault( node, [] )
-
-    def add_edge( self, src, dest ):
-        "Add edge to graph"
-        src, dest = sorted( ( src, dest ) )
-        self.add_node( src )
-        self.add_node( dest )
-        self.data[ src ].append( dest )
+        self.node = {}
+        self.edge = {}
+
+    def add_node( self, node, attr_dict=None, **attrs):
+        """Add node to graph
+           attr_dict: attribute dict (optional)
+           attrs: more attributes (optional)"""
+        attr_dict = {} if attr_dict is None else attr_dict
+        attr_dict.update( attrs )
+        self.node[ node ] = attr_dict
+
+    def add_edge( self, src, dst, key=None, attr_dict=None, **attrs ):
+        """Add edge to graph
+           key: optional key
+           attr_dict: optional attribute dict"""
+        attr_dict = {} if attr_dict is None else attr_dict
+        attr_dict.update( attrs )
+        self.node.setdefault( src, {} )
+        self.node.setdefault( dst, {} )
+        self.edge.setdefault( src, {} )
+        self.edge.setdefault( dst, {} )
+        self.edge[ src ].setdefault( dst, {} )
+        entry = self.edge[ dst ][ src ] = self.edge[ src ][ dst ]
+        # If no key, pick next ordinal number
+        if key is None:
+            keys = [ k for k in entry.keys() if type( k ) is int ]
+            key = max( [ 0 ] + keys ) + 1
+        entry[ key ] = attr_dict
+        return key
 
-    def nodes( self ):
-        "Return list of graph nodes"
-        return self.data.keys()
+    def nodes( self, data=False):
+        """Return list of graph nodes
+           data: return list of ( node, attrs)"""
+        return self.node.items() if data else self.node.keys()
 
-    def edges( self ):
+    def edges_iter( self, data=False, keys=False ):
         "Iterator: return graph edges"
-        for src in self.data.keys():
-            for dest in self.data[ src ]:
-                yield ( src, dest )
+        for src, entry in self.edge.iteritems():
+            for dst, keys in entry.iteritems():
+                if src > dst:
+                    # Skip duplicate edges
+                    continue
+                for k, attrs in keys.iteritems():
+                    if data:
+                        if keys:
+                            yield( src, dst, k, attrs )
+                        else:
+                            yield( src, dst, attrs )
+                    else:
+                        if keys:
+                            yield( src, dst, k )
+                        else:
+                            yield( src, dst )
+
+    def edges( self, data=False, keys=False ):
+        "Return list of graph edges"
+        return list( self.edges_iter( data=data, keys=keys ) )
+
 
     def __getitem__( self, node ):
-        "Return link dict for the given node"
-        return self.data[node]
+        "Return link dict for given src node"
+        return self.edge[ node ]
 
+    def __len__( self ):
+        "Return the number of nodes"
+        return len( self.node )
 
-class Topo(object):
+    def convertTo( self, cls, data=False, keys=False ):
+        """Convert to a new object of networkx.MultiGraph-like class cls
+           data: include node and edge data
+           keys: include edge keys as well as edge data"""
+        g = cls()
+        g.add_nodes_from( self.nodes( data=data ) )
+        g.add_edges_from( self.edges( data=( data or keys ), keys=keys ) )
+        return g
+
+
+class Topo( object ):
     "Data center network representation for structured multi-trees."
 
-    def __init__(self, *args, **params):
+    def __init__( self, *args, **params ):
         """Topo object. 
            Optional named parameters:
            hinfo: default host options
@@ -56,150 +105,182 @@ def __init__(self, *args, **params):
            lopts: default link options
            calls build()"""
         self.g = MultiGraph()
-        self.node_info = {}
-        self.link_info = {}  # (src, dst) tuples hash to EdgeInfo objects
         self.hopts = params.pop( 'hopts', {} )
         self.sopts = params.pop( 'sopts', {} )
         self.lopts = params.pop( 'lopts', {} )
-        self.ports = {}  # ports[src][dst] is port on src that connects to dst
+        self.ports = {}  # ports[src][dst][sport] is port on dst that connects to src
         self.build( *args, **params )
 
     def build( self, *args, **params ):
         "Override this method to build your topology."
         pass
 
-    def addNode(self, name, **opts):
+    def addNode( self, name, **opts ):
         """Add Node to graph.
            name: name
            opts: node options
            returns: node name"""
-        self.g.add_node(name)
-        self.node_info[name] = opts
+        self.g.add_node( name, **opts )
         return name
 
-    def addHost(self, name, **opts):
+    def addHost( self, name, **opts ):
         """Convenience method: Add host to graph.
            name: host name
            opts: host options
            returns: host name"""
         if not opts and self.hopts:
             opts = self.hopts
-        return self.addNode(name, **opts)
+        return self.addNode( name, **opts )
 
-    def addSwitch(self, name, **opts):
+    def addSwitch( self, name, **opts ):
         """Convenience method: Add switch to graph.
            name: switch name
            opts: switch options
            returns: switch name"""
         if not opts and self.sopts:
             opts = self.sopts
-        result = self.addNode(name, isSwitch=True, **opts)
+        result = self.addNode( name, isSwitch=True, **opts )
         return result
 
-    def addLink(self, node1, node2, port1=None, port2=None,
-                **opts):
+    def addLink( self, node1, node2, port1=None, port2=None,
+                key=None, **opts ):
         """node1, node2: nodes to link together
            port1, port2: ports (optional)
            opts: link options (optional)
            returns: link info key"""
         if not opts and self.lopts:
             opts = self.lopts
-        self.addPort(node1, node2, port1, port2)
-        key = tuple(self.sorted([node1, node2]))
-        self.link_info[key] = opts
-        self.g.add_edge(*key)
+        port1, port2 = self.addPort( node1, node2, port1, port2 )
+        opts.update( node1=node1, node2=node2, port1=port1, port2=port2 )
+        self.g.add_edge(node1, node2, key, opts )
         return key
 
-    def addPort(self, src, dst, sport=None, dport=None):
-        '''Generate port mapping for new edge.
-        @param src source switch name
-        @param dst destination switch name
-        '''
-        self.ports.setdefault(src, {})
-        self.ports.setdefault(dst, {})
-        # New port: number of outlinks + base
-        src_base = 1 if self.isSwitch(src) else 0
-        dst_base = 1 if self.isSwitch(dst) else 0
-        if sport is None:
-            sport = len(self.ports[src]) + src_base
-        if dport is None:
-            dport = len(self.ports[dst]) + dst_base
-        self.ports[src][dst] = sport
-        self.ports[dst][src] = dport
-
-    def nodes(self, sort=True):
+    def nodes( self, sort=True ):
         "Return nodes in graph"
         if sort:
             return self.sorted( self.g.nodes() )
         else:
             return self.g.nodes()
 
-    def isSwitch(self, n):
-        '''Returns true if node is a switch.'''
-        info = self.node_info[n]
-        return info and info.get('isSwitch', False)
-
-    def switches(self, sort=True):
-        '''Return switches.
-        sort: sort switches alphabetically
-        @return dpids list of dpids
-        '''
-        return [n for n in self.nodes(sort) if self.isSwitch(n)]
-
-    def hosts(self, sort=True):
-        '''Return hosts.
-        sort: sort hosts alphabetically
-        @return dpids list of dpids
-        '''
-        return [n for n in self.nodes(sort) if not self.isSwitch(n)]
-
-    def links(self, sort=True):
-        '''Return links.
-        sort: sort links alphabetically
-        @return links list of name pairs
-        '''
-        if not sort:
-            return self.g.edges()
-        else:
-            links = [tuple(self.sorted(e)) for e in self.g.edges()]
-            return sorted( links, key=naturalSeq )
-
-    def port(self, src, dst):
-        '''Get port number.
-
-        @param src source switch name
-        @param dst destination switch name
-        @return tuple (src_port, dst_port):
-            src_port: port on source switch leading to the destination switch
-            dst_port: port on destination switch leading to the source switch
-        '''
-        if src in self.ports and dst in self.ports[src]:
-            assert dst in self.ports and src in self.ports[dst]
-            return self.ports[src][dst], self.ports[dst][src]
-
-    def linkInfo( self, src, dst ):
-        "Return link metadata"
-        src, dst = self.sorted([src, dst])
-        return self.link_info[(src, dst)]
-
-    def setlinkInfo( self, src, dst, info ):
-        "Set link metadata"
-        src, dst = self.sorted([src, dst])
-        self.link_info[(src, dst)] = info
+    def isSwitch( self, n ):
+        "Returns true if node is a switch."
+        return self.g.node[ n ].get( 'isSwitch', False )
+
+    def switches( self, sort=True ):
+        """Return switches.
+           sort: sort switches alphabetically
+           returns: dpids list of dpids"""
+        return [ n for n in self.nodes( sort ) if self.isSwitch( n ) ]
+
+    def hosts( self, sort=True ):
+        """Return hosts.
+           sort: sort hosts alphabetically
+           returns: list of hosts"""
+        return [ n for n in self.nodes( sort ) if not self.isSwitch( n ) ]
+
+    def iterLinks( self, withKeys=False, withInfo=False ):
+        """Return links (iterator)
+           withKeys: return link keys
+           withInfo: return link info
+           returns: list of ( src, dst [,key, info ] )"""
+        for src, dst, key, info in self.g.edges_iter( data=True, keys=True ):
+            node1, node2 = info[ 'node1' ], info[ 'node2' ]
+            if withKeys:
+                if withInfo:
+                    yield( node1, node2, key, info )
+                else:
+                    yield( node1, node2, key )
+            else:
+                if withInfo:
+                    yield( node1, node2, info )
+                else:
+                    yield( node1, node2 )
+
+    def links( self, sort=False, withKeys=False, withInfo=False ):
+        """Return links
+           sort: sort links alphabetically, preserving (src, dst) order
+           withKeys: return link keys
+           withInfo: return link info
+           returns: list of ( src, dst [,key, info ] )"""
+        links = list( self.iterLinks( withKeys, withInfo ) )
+        if not sorted:
+            return links
+        # Ignore info when sorting
+        tupleSize = 3 if withKeys else 2
+        return sorted( links, key=( lambda l: naturalSeq( l[ :tupleSize ] ) ) )
+
+    # This legacy port management mechanism is clunky and will probably
+    # be removed at some point.
+
+    def addPort( self, src, dst, sport=None, dport=None ):
+        """Generate port mapping for new edge.
+            src: source switch name
+            dst: destination switch name"""
+        # Initialize if necessary
+        ports = self.ports
+        ports.setdefault( src, {} )
+        ports.setdefault( dst, {} )
+        # New port: number of outlinks + base
+        if sport is None:
+            src_base = 1 if self.isSwitch( src ) else 0
+            sport = len( ports[ src ] ) + src_base
+        if dport is None:
+            dst_base = 1 if self.isSwitch( dst ) else 0
+            dport = len( ports[ dst ] ) + dst_base
+        ports[ src ][ sport ] = ( dst, dport )
+        ports[ dst ][ dport ] = ( src, sport )
+        return sport, dport
+
+    def port( self, src, dst ):
+        """Get port numbers.
+            src: source switch name
+            dst: destination switch name
+            sport: optional source port (otherwise use lowest src port)
+            returns: tuple (sport, dport), where
+                sport = port on source switch leading to the destination switch
+                dport = port on destination switch leading to the source switch
+            Note that you can also look up ports using linkInfo()"""
+        # A bit ugly and slow vs. single-link implementation ;-(
+        ports = [ ( sport, entry[ 1 ] )
+                  for sport, entry in self.ports[ src ].items()
+                  if entry[ 0 ] == dst ]
+        return ports if len( ports ) != 1 else ports[ 0 ]
+
+    def _linkEntry( self, src, dst, key=None ):
+        "Helper function: return link entry and key"
+        entry = self.g[ src ][ dst ]
+        if key is None:
+            key = min( entry )
+        return entry, key
+    
+    def linkInfo( self, src, dst, key=None ):
+        "Return link metadata dict"
+        entry, key = self._linkEntry( src, dst, key )
+        return entry[ key ]
+
+    def setlinkInfo( self, src, dst, info, key=None ):
+        "Set link metadata dict"
+        entry, key = self._linkEntry( src, dst, key )
+        entry[ key ] = info
 
     def nodeInfo( self, name ):
         "Return metadata (dict) for node"
-        info = self.node_info[ name ]
-        return info if info is not None else {}
+        return self.g.node[ name ]
 
     def setNodeInfo( self, name, info ):
         "Set metadata (dict) for node"
-        self.node_info[ name ] = info
+        self.g.node[ name ] = info
+
+    def convertTo( self, cls, data=True, keys=True ):
+        """Convert to a new object of networkx.MultiGraph-like class cls
+           data: include node and edge data (default True)
+           keys: include edge keys as well as edge data (default True)"""
+        return self.g.convertTo( cls, data=data, keys=keys )
 
     @staticmethod
     def sorted( items ):
         "Items sorted in natural (i.e. alphabetical) order"
-        return sorted(items, key=natural)
+        return sorted( items, key=natural )
 
 
 class SingleSwitchTopo( Topo ):
@@ -228,6 +309,7 @@ def build( self, k=2 ):
             self.addLink( host, switch,
                           port1=0, port2=( k - h + 1 ) )
 
+
 class LinearTopo( Topo ):
     "Linear topology of k switches, with n hosts per switch."
 
diff --git a/mininet/util.py b/mininet/util.py
index bb1935f0ce2a438233555ad0ec996a405a6cebc8..0c76a9995d174ffe57ff0a30c59eb8e360c5cbff 100644
--- a/mininet/util.py
+++ b/mininet/util.py
@@ -448,7 +448,7 @@ def natural( text ):
     def num( s ):
         "Convert text segment to int if necessary"
         return int( s ) if s.isdigit() else s
-    return [  num( s ) for s in re.split( r'(\d+)', text ) ]
+    return [  num( s ) for s in re.split( r'(\d+)', str( text ) ) ]
 
 def naturalSeq( t ):
     "Natural sort key function for sequences"