- TAP interfaces
authorClaudio-Daniel Freire <claudio-daniel.freire@inria.fr>
Wed, 4 May 2011 10:38:03 +0000 (12:38 +0200)
committerClaudio-Daniel Freire <claudio-daniel.freire@inria.fr>
Wed, 4 May 2011 10:38:03 +0000 (12:38 +0200)
- TAP tunnelling
- Encrypted tunnels
- UDP-based tunnels

src/nepi/core/metadata.py
src/nepi/testbeds/planetlab/execute.py
src/nepi/testbeds/planetlab/interfaces.py
src/nepi/testbeds/planetlab/metadata_v01.py
src/nepi/testbeds/planetlab/scripts/tun_connect.py
src/nepi/testbeds/planetlab/tunproto.py
test/testbeds/planetlab/execute.py

index 0b30a41..3fbf783 100644 (file)
@@ -258,6 +258,15 @@ class Metadata(object):
                 "flags": Attribute.Invisible | Attribute.ReadOnly,
                 "validation_function": validation.is_string,
             }),
+            "tun_key" : dict({
+                "name": "tun_key", 
+                "help": "Randomly selected TUNneling protocol cryptographic key. "
+                        "Endpoints must agree to use the minimum (in lexicographic order) "
+                        "of both the remote and local sides.",
+                "type": Attribute.STRING,
+                "flags": Attribute.Invisible | Attribute.ReadOnly,
+                "validation_function": validation.is_string,
+            }),
             "tun_addr" : dict({
                 "name": "tun_addr", 
                 "help": "IP address of the tunnel endpoint",
index 8aecc76..7f3e305 100644 (file)
@@ -207,6 +207,9 @@ class TestbedController(testbed_impl.TestbedController):
     def _make_tun_iface(self, parameters):
         return self._make_generic(parameters, self._interfaces.TunIface)
 
+    def _make_tap_iface(self, parameters):
+        return self._make_generic(parameters, self._interfaces.TapIface)
+
     def _make_netpipe(self, parameters):
         return self._make_generic(parameters, self._interfaces.NetPipe)
 
index 8e2a401..014ccee 100644 (file)
@@ -8,6 +8,7 @@ import plcapi
 import subprocess
 import os
 import os.path
+import random
 
 import tunproto
 
@@ -33,6 +34,15 @@ class NodeIface(object):
 
         # These get initialized when the iface is connected to the internet
         self.has_internet = False
+        
+        # Generate an initial random cryptographic key to use for tunnelling
+        # Upon connection, both endpoints will agree on a common one based on
+        # this one.
+        self.tun_key = ( ''.join(map(chr, [ 
+                    r.getrandbits(8) 
+                    for i in xrange(32) 
+                    for r in (random.SystemRandom(),) ])
+                ).encode("base64").strip() )        
 
     def __str__(self):
         return "%s<ip:%s/%s up mac:%s>" % (
@@ -90,6 +100,9 @@ class _CrossIface(object):
         self.tun_port = port
 
 class TunIface(object):
+    _PROTO_MAP = tunproto.TUN_PROTO_MAP
+    _KIND = 'TUN'
+
     def __init__(self, api=None):
         if not api:
             api = plcapi.PLCAPI()
@@ -119,6 +132,7 @@ class TunIface(object):
         # They're part of the TUN standard attribute set
         self.tun_port = None
         self.tun_addr = None
+        self.tun_key = None
         
         # These get initialized when the iface is connected to its peer
         self.peer_iface = None
@@ -138,9 +152,9 @@ class TunIface(object):
 
     def add_address(self, address, netprefix, broadcast):
         if (self.address or self.netprefix or self.netmask) is not None:
-            raise RuntimeError, "Cannot add more than one address to TUN interfaces"
+            raise RuntimeError, "Cannot add more than one address to %s interfaces" % (self._KIND,)
         if broadcast:
-            raise ValueError, "TUN interfaces cannot broadcast in PlanetLab"
+            raise ValueError, "%s interfaces cannot broadcast in PlanetLab" % (self._KIND,)
         
         self.address = address
         self.netprefix = netprefix
@@ -148,11 +162,11 @@ class TunIface(object):
     
     def validate(self):
         if not self.node:
-            raise RuntimeError, "Unconnected TUN iface - missing node"
-        if self.peer_iface and self.peer_proto not in tunproto.PROTO_MAP:
+            raise RuntimeError, "Unconnected %s iface - missing node" % (self._KIND,)
+        if self.peer_iface and self.peer_proto not in self._PROTO_MAP:
             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
         if not self.address or not self.netprefix or not self.netmask:
-            raise RuntimeError, "Misconfigured TUN iface - missing address"
+            raise RuntimeError, "Misconfigured %s iface - missing address" % (self._KIND,)
     
     def prepare(self, home_path, listening):
         if not self.peer_iface and (self.peer_proto and (listening or (self.peer_addr and self.peer_port))):
@@ -163,8 +177,8 @@ class TunIface(object):
                 self.peer_port)
         if self.peer_iface:
             if not self.peer_proto_impl:
-                self.peer_proto_impl = tunproto.PROTO_MAP[self.peer_proto](
-                    self, self.peer_iface, home_path, listening)
+                self.peer_proto_impl = self._PROTO_MAP[self.peer_proto](
+                    self, self.peer_iface, home_path, self.tun_key, listening)
                 self.peer_proto_impl.port = self.tun_port
             self.peer_proto_impl.prepare()
     
@@ -183,6 +197,10 @@ class TunIface(object):
         else:
             return None
 
+class TapIface(TunIface):
+    _PROTO_MAP = tunproto.TAP_PROTO_MAP
+    _KIND = 'TAP'
+
 # Yep, it does nothing - yet
 class Internet(object):
     def __init__(self, api=None):
index 6cb15c7..81718da 100644 (file)
@@ -17,6 +17,7 @@ import os.path
 NODE = "Node"
 NODEIFACE = "NodeInterface"
 TUNIFACE = "TunInterface"
+TAPIFACE = "TapInterface"
 APPLICATION = "Application"
 DEPENDENCY = "Dependency"
 NEPIDEPENDENCY = "NepiDependency"
@@ -95,6 +96,7 @@ def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
         raise RuntimeError, "Use of TUN interfaces requires emulation"
     iface.node = node
     node.required_vsys.update(('fd_tuntap', 'vif_up'))
+    node.required_packages.add('python-crypto')
 
 def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
     iface = testbed_instance._elements[iface_guid]
@@ -102,6 +104,7 @@ def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid)
     iface.peer_iface = peer_iface
     iface.peer_proto = \
     iface.tun_proto = proto
+    iface.tun_key = peer_iface.tun_key
 
 def crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data):
     iface = testbed_instance._elements[iface_guid]
@@ -109,6 +112,7 @@ def crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_i
     iface.peer_addr = peer_iface_data.get("tun_addr")
     iface.peer_proto = peer_iface_data.get("tun_proto")
     iface.peer_port = peer_iface_data.get("tun_port")
+    iface.tun_key = min(iface.tun_key, peer_iface_data.get("tun_key"))
     iface.tun_proto = proto
     
     preconfigure_tuniface(testbed_instance, iface_guid)
@@ -170,6 +174,11 @@ def create_tuniface(testbed_instance, guid):
     element = testbed_instance._make_tun_iface(parameters)
     testbed_instance.elements[guid] = element
 
+def create_tapiface(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_tap_iface(parameters)
+    testbed_instance.elements[guid] = element
+
 def create_application(testbed_instance, guid):
     parameters = testbed_instance._get_parameters(guid)
     element = testbed_instance._make_application(parameters)
@@ -443,6 +452,12 @@ connections = [
         "init_code": connect_tun_iface_node,
         "can_cross": False
     }),
+    dict({
+        "from": (TESTBED_ID, NODE, "devs"),
+        "to":   (TESTBED_ID, TAPIFACE, "node"),
+        "init_code": connect_tun_iface_node,
+        "can_cross": False
+    }),
     dict({
         "from": (TESTBED_ID, NODEIFACE, "inet"),
         "to":   (TESTBED_ID, INTERNET, "devs"),
@@ -491,6 +506,18 @@ connections = [
         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
         "can_cross": False
     }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "tcp"),
+        "to":   (TESTBED_ID, TAPIFACE, "tcp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "udp"),
+        "to":   (TESTBED_ID, TAPIFACE, "udp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"udp"),
+        "can_cross": False
+    }),
     dict({
         "from": (TESTBED_ID, TUNIFACE, "tcp"),
         "to":   (None, None, "tcp"),
@@ -505,6 +532,20 @@ connections = [
         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
         "can_cross": True
     }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "tcp"),
+        "to":   (None, None, "tcp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "udp"),
+        "to":   (None, None, "udp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
+        "can_cross": True
+    }),
 ]
 
 attributes = dict({
@@ -819,9 +860,9 @@ traces = dict({
               }),
     })
 
-create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
+create_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
 
-configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
+configure_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
 
 factories_info = dict({
     NODE: dict({
@@ -858,11 +899,26 @@ factories_info = dict({
         }),
     TUNIFACE: dict({
             "allow_addresses": True,
-            "help": "Virtual TUN network interface",
+            "help": "Virtual TUN network interface (layer 3)",
             "category": "devices",
             "create_function": create_tuniface,
             "preconfigure_function": preconfigure_tuniface,
             "configure_function": postconfigure_tuniface,
+            "box_attributes": [
+                "up", "device_name", "mtu", "snat",
+                "txqueuelen",
+                "tun_proto", "tun_addr", "tun_port", "tun_key"
+            ],
+            "traces": ["packets"],
+            "connector_types": ["node","udp","tcp"]
+        }),
+    TAPIFACE: dict({
+            "allow_addresses": True,
+            "help": "Virtual TAP network interface (layer 2)",
+            "category": "devices",
+            "create_function": create_tapiface,
+            "preconfigure_function": preconfigure_tuniface,
+            "configure_function": postconfigure_tuniface,
             "box_attributes": [
                 "up", "device_name", "mtu", "snat",
                 "txqueuelen",
index 59925dd..415423c 100644 (file)
@@ -71,6 +71,18 @@ parser.add_option(
     default = None,
     help = 
         "See mode. This specifies the interface's transmission queue length. " )
+parser.add_option(
+    "-u", "--udp", dest="udp", metavar="PORT", type="int",
+    default = None,
+    help = 
+        "Bind to the specified UDP port locally, and send UDP datagrams to the "
+        "remote endpoint, creating a tunnel through UDP rather than TCP." )
+parser.add_option(
+    "-k", "--key", dest="cipher_key", metavar="KEY",
+    default = None,
+    help = 
+        "Specify a symmetric encryption key with which to protect packets across "
+        "the tunnel. python-crypto must be installed on the system." )
 
 (options, remaining_args) = parser.parse_args(sys.argv[1:])
 
@@ -356,6 +368,27 @@ def piWrap(buf, ether_mode):
         +buf
     )
 
+def encrypt(packet, crypter):
+    # pad
+    padding = crypter.block_size - len(packet) % crypter.block_size
+    packet += chr(padding) * padding
+    
+    # encrypt
+    return crypter.encrypt(packet)
+
+def decrypt(packet, crypter):
+    # decrypt
+    packet = crypter.decrypt(packet)
+    
+    # un-pad
+    padding = ord(packet[-1])
+    if not (0 < padding <= crypter.block_size):
+        # wrong padding
+        raise RuntimeError, "Truncated packet"
+    packet = packet[:-padding]
+    
+    return packet
+
 abortme = False
 def tun_fwd(tun, remote):
     global abortme
@@ -365,6 +398,28 @@ def tun_fwd(tun, remote):
     with_pi = options.mode.startswith('pl-')
     ether_mode = tun_name.startswith('tap')
     
+    crypto_mode = False
+    try:
+        if options.cipher_key:
+            import Crypto.Cipher.AES
+            import hashlib
+            
+            hashed_key = hashlib.sha256(options.cipher_key).digest()
+            crypter = Crypto.Cipher.AES.new(
+                hashed_key, 
+                Crypto.Cipher.AES.MODE_ECB)
+            crypto_mode = True
+    except:
+        import traceback
+        traceback.print_exc()
+        crypto_mode = False
+        crypter = None
+
+    if crypto_mode:
+        print >>sys.stderr, "Packets are transmitted in CIPHER"
+    else:
+        print >>sys.stderr, "Packets are transmitted in PLAINTEXT"
+    
     # Limited frame parsing, to preserve packet boundaries.
     # Which is needed, since /dev/net/tun is unbuffered
     fwbuf = ""
@@ -384,7 +439,16 @@ def tun_fwd(tun, remote):
         # check to see if we can write
         if remote in wrdy and packetReady(fwbuf, ether_mode):
             packet, fwbuf = pullPacket(fwbuf, ether_mode)
-            os.write(remote.fileno(), packet)
+            try:
+                if crypto_mode:
+                    enpacket = encrypt(packet, crypter)
+                else:
+                    enpacket = packet
+                os.write(remote.fileno(), enpacket)
+            except:
+                if not options.udp:
+                    # in UDP mode, we ignore errors - packet loss man...
+                    raise
             print >>sys.stderr, '>', formatPacket(packet, ether_mode)
         if tun in wrdy and packetReady(bkbuf, ether_mode):
             packet, bkbuf = pullPacket(bkbuf, ether_mode)
@@ -401,7 +465,14 @@ def tun_fwd(tun, remote):
                 packet = piStrip(packet)
             fwbuf += packet
         if remote in rdrdy:
-            packet = os.read(remote.fileno(),2000) # remote.read blocks until it gets 2k!
+            try:
+                packet = os.read(remote.fileno(),2000) # remote.read blocks until it gets 2k!
+                if crypto_mode:
+                    packet = decrypt(packet, crypter)
+            except:
+                if not options.udp:
+                    # in UDP mode, we ignore errors - packet loss man...
+                    raise
             bkbuf += packet
 
 
@@ -455,18 +526,31 @@ except:
 
 
 try:
-    # connect to remote endpoint
-    if remaining_args and not remaining_args[0].startswith('-'):
-        print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
-        rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-        rsock.connect((remaining_args[0],options.port))
+    if options.udp:
+        # connect to remote endpoint
+        if remaining_args and not remaining_args[0].startswith('-'):
+            print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.udp)
+            print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
+            rsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
+            rsock.bind((hostaddr,options.udp))
+            rsock.connect((remaining_args[0],options.port))
+        else:
+            print >>sys.stderr, "Error: need a remote endpoint in UDP mode"
+            raise AssertionError, "Error: need a remote endpoint in UDP mode"
+        remote = os.fdopen(rsock.fileno(), 'r+b', 0)
     else:
-        print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.port)
-        lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-        lsock.bind((hostaddr,options.port))
-        lsock.listen(1)
-        rsock,raddr = lsock.accept()
-    remote = os.fdopen(rsock.fileno(), 'r+b', 0)
+        # connect to remote endpoint
+        if remaining_args and not remaining_args[0].startswith('-'):
+            print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
+            rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+            rsock.connect((remaining_args[0],options.port))
+        else:
+            print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.port)
+            lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+            lsock.bind((hostaddr,options.port))
+            lsock.listen(1)
+            rsock,raddr = lsock.accept()
+        remote = os.fdopen(rsock.fileno(), 'r+b', 0)
 
     print >>sys.stderr, "Connected"
 
index 5809b68..03293f2 100644 (file)
@@ -11,7 +11,7 @@ import threading
 from nepi.util import server
 
 class TunProtoBase(object):
-    def __init__(self, local, peer, home_path):
+    def __init__(self, local, peer, home_path, key):
         # Weak references, since ifaces do have a reference to the
         # tunneling protocol implementation - we don't want strong
         # circular references.
@@ -20,6 +20,7 @@ class TunProtoBase(object):
         
         self.port = 15000
         self.mode = 'pl-tun'
+        self.key = key
         
         self.home_path = home_path
         
@@ -124,7 +125,8 @@ class TunProtoBase(object):
             "-m", str(self.mode),
             "-p", str(local_port if listen else peer_port),
             "-A", str(local_addr),
-            "-M", str(local_mask)]
+            "-M", str(local_mask),
+            "-k", str(self.key)]
         
         if local_snat:
             args.append("-S")
@@ -304,22 +306,22 @@ class TunProtoBase(object):
         
 
 class TunProtoUDP(TunProtoBase):
-    def __init__(self, local, peer, home_path, listening):
-        super(TunProtoTCP, self).__init__(local, peer, home_path)
+    def __init__(self, local, peer, home_path, key, listening):
+        super(TunProtoUDP, self).__init__(local, peer, home_path, key)
         self.listening = listening
     
     def prepare(self):
         pass
     
     def setup(self):
-        self.launch_async('udp', False, ("-U",))
+        self.async_launch('udp', False, ("-u",str(self.port)))
     
     def shutdown(self):
         self.kill()
 
 class TunProtoTCP(TunProtoBase):
-    def __init__(self, local, peer, home_path, listening):
-        super(TunProtoTCP, self).__init__(local, peer, home_path)
+    def __init__(self, local, peer, home_path, key, listening):
+        super(TunProtoTCP, self).__init__(local, peer, home_path, key)
         self.listening = listening
     
     def prepare(self):
@@ -343,9 +345,26 @@ class TunProtoTCP(TunProtoBase):
     def shutdown(self):
         self.kill()
 
-PROTO_MAP = {
+class TapProtoUDP(TunProtoUDP):
+    def __init__(self, local, peer, home_path, key, listening):
+        super(TapProtoUDP, self).__init__(local, peer, home_path, key, listening)
+        self.mode = 'pl-tap'
+
+class TapProtoTCP(TunProtoTCP):
+    def __init__(self, local, peer, home_path, key, listening):
+        super(TapProtoTCP, self).__init__(local, peer, home_path, key, listening)
+        self.mode = 'pl-tap'
+
+
+
+TUN_PROTO_MAP = {
     'tcp' : TunProtoTCP,
     'udp' : TunProtoUDP,
 }
 
+TAP_PROTO_MAP = {
+    'tcp' : TapProtoTCP,
+    'udp' : TapProtoUDP,
+}
+
 
index 627e256..cef57b7 100755 (executable)
@@ -325,7 +325,7 @@ echo 'OKIDOKI'
             pass
 
     @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
-    def test_tun_ping(self):
+    def _pingtest(self, TunClass, ConnectionProto):
         instance = self.make_instance()
         
         instance.defer_create(2, "Node")
@@ -341,13 +341,13 @@ echo 'OKIDOKI'
         instance.defer_create(6, "NodeInterface")
         instance.defer_connect(3, "devs", 6, "node")
         instance.defer_connect(6, "inet", 5, "devs")
-        instance.defer_create(7, "TunInterface")
+        instance.defer_create(7, TunClass)
         instance.defer_add_address(7, "192.168.2.2", 24, False)
         instance.defer_connect(2, "devs", 7, "node")
-        instance.defer_create(8, "TunInterface")
+        instance.defer_create(8, TunClass)
         instance.defer_add_address(8, "192.168.2.3", 24, False)
         instance.defer_connect(3, "devs", 8, "node")
-        instance.defer_connect(7, "tcp", 8, "tcp")
+        instance.defer_connect(7, ConnectionProto, 8, ConnectionProto)
         instance.defer_create(9, "Application")
         instance.defer_create_set(9, "command", "ping -qc1 {#[GUID-8].addr[0].[Address]#}")
         instance.defer_add_trace(9, "stdout")
@@ -388,6 +388,22 @@ echo 'OKIDOKI'
         self.assertTrue(re.match(comp_result, ping_result, re.MULTILINE),
             "Unexpected trace:\n" + ping_result)
 
+    @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
+    def test_tun_ping(self):
+        self._pingtest("TunInterface", "tcp")
+
+    @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
+    def test_tun_ping_udp(self):
+        self._pingtest("TunInterface", "udp")
+
+    @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
+    def test_tap_ping(self):
+        self._pingtest("TapInterface", "tcp")
+
+    @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
+    def test_tap_ping_udp(self):
+        self._pingtest("TapInterface", "udp")
+
     @test_util.skipUnless(test_util.pl_auth() is not None, "Test requires PlanetLab authentication info (PL_USER and PL_PASS environment variables)")
     def test_nepi_depends(self):
         instance = self.make_instance()