From ef968d674bcacf6ab6e42eaf1f6f376278645529 Mon Sep 17 00:00:00 2001 From: Claudio-Daniel Freire Date: Wed, 4 May 2011 12:38:03 +0200 Subject: [PATCH] - TAP interfaces - TAP tunnelling - Encrypted tunnels - UDP-based tunnels --- src/nepi/core/metadata.py | 9 ++ src/nepi/testbeds/planetlab/execute.py | 3 + src/nepi/testbeds/planetlab/interfaces.py | 32 +++-- src/nepi/testbeds/planetlab/metadata_v01.py | 62 +++++++++- .../testbeds/planetlab/scripts/tun_connect.py | 110 +++++++++++++++--- src/nepi/testbeds/planetlab/tunproto.py | 35 ++++-- test/testbeds/planetlab/execute.py | 24 +++- 7 files changed, 240 insertions(+), 35 deletions(-) diff --git a/src/nepi/core/metadata.py b/src/nepi/core/metadata.py index 0b30a411..3fbf7831 100644 --- a/src/nepi/core/metadata.py +++ b/src/nepi/core/metadata.py @@ -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", diff --git a/src/nepi/testbeds/planetlab/execute.py b/src/nepi/testbeds/planetlab/execute.py index 8aecc76a..7f3e3050 100644 --- a/src/nepi/testbeds/planetlab/execute.py +++ b/src/nepi/testbeds/planetlab/execute.py @@ -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) diff --git a/src/nepi/testbeds/planetlab/interfaces.py b/src/nepi/testbeds/planetlab/interfaces.py index 8e2a4015..014ccee8 100644 --- a/src/nepi/testbeds/planetlab/interfaces.py +++ b/src/nepi/testbeds/planetlab/interfaces.py @@ -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" % ( @@ -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): diff --git a/src/nepi/testbeds/planetlab/metadata_v01.py b/src/nepi/testbeds/planetlab/metadata_v01.py index 6cb15c7a..81718da0 100644 --- a/src/nepi/testbeds/planetlab/metadata_v01.py +++ b/src/nepi/testbeds/planetlab/metadata_v01.py @@ -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", diff --git a/src/nepi/testbeds/planetlab/scripts/tun_connect.py b/src/nepi/testbeds/planetlab/scripts/tun_connect.py index 59925dd1..415423cb 100644 --- a/src/nepi/testbeds/planetlab/scripts/tun_connect.py +++ b/src/nepi/testbeds/planetlab/scripts/tun_connect.py @@ -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" diff --git a/src/nepi/testbeds/planetlab/tunproto.py b/src/nepi/testbeds/planetlab/tunproto.py index 5809b684..03293f26 100644 --- a/src/nepi/testbeds/planetlab/tunproto.py +++ b/src/nepi/testbeds/planetlab/tunproto.py @@ -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, +} + diff --git a/test/testbeds/planetlab/execute.py b/test/testbeds/planetlab/execute.py index 627e256a..cef57b74 100755 --- a/test/testbeds/planetlab/execute.py +++ b/test/testbeds/planetlab/execute.py @@ -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() -- 2.43.0