From: Alina Quereilhac Date: Wed, 30 Jul 2014 17:01:05 +0000 (+0200) Subject: Progress with LinuxTAP and UDP connections X-Git-Tag: nepi-3.2.0~107 X-Git-Url: http://git.onelab.eu/?p=nepi.git;a=commitdiff_plain;h=f7c6792813947d3707accd690ad7bbe83277798f Progress with LinuxTAP and UDP connections --- diff --git a/src/nepi/execution/resource.py b/src/nepi/execution/resource.py index e82ced04..5ec0be38 100644 --- a/src/nepi/execution/resource.py +++ b/src/nepi/execution/resource.py @@ -749,6 +749,18 @@ class ResourceManager(Logger): connected.append(rm) return connected + def is_rm_instance(self, rtype): + """ Returns True if the RM is instance of 'rtype' + + :param rtype: Type of the RM we look for + :type rtype: str + :return: True|False + """ + rclass = ResourceFactory.get_resource_type(rtype) + if isinstance(self, rclass): + return True + return False + @failtrap def _needs_reschedule(self, group, state, time): """ Internal method that verify if 'time' has elapsed since diff --git a/src/nepi/resources/linux/application.py b/src/nepi/resources/linux/application.py index 86cf9c98..08ea7004 100644 --- a/src/nepi/resources/linux/application.py +++ b/src/nepi/resources/linux/application.py @@ -711,6 +711,7 @@ class LinuxApplication(ResourceManager): def execute_command(self, command, env = None, sudo = False, + tty = False, forward_x11 = False, blocking = False): @@ -722,6 +723,7 @@ class LinuxApplication(ResourceManager): return self.node.execute(command, sudo = sudo, + tty = tty, forward_x11 = forward_x11, blocking = blocking) diff --git a/src/nepi/resources/linux/scripts/linux-udp-connect.py b/src/nepi/resources/linux/scripts/linux-udp-connect.py new file mode 100644 index 00000000..ce869f7c --- /dev/null +++ b/src/nepi/resources/linux/scripts/linux-udp-connect.py @@ -0,0 +1,191 @@ +import errno +import os +import time +import signal +import socket +import tunchannel +import struct +import fcntl + +from optparse import OptionParser + +IFF_TUN = 0x0001 +IFF_TAP = 0x0002 +IFF_NO_PI = 0x1000 +TUNSETIFF = 0x400454ca + +# Trak SIGTERM, and set global termination flag instead of dying +TERMINATE = [] +def _finalize(sig,frame): + global TERMINATE + TERMINATE.append(None) +signal.signal(signal.SIGTERM, _finalize) + +# SIGUSR1 suspends forwading, SIGUSR2 resumes forwarding +SUSPEND = [] +def _suspend(sig,frame): + global SUSPEND + if not SUSPEND: + SUSPEND.append(None) +signal.signal(signal.SIGUSR1, _suspend) + +def _resume(sig,frame): + global SUSPEND + if SUSPEND: + SUSPEND.remove(None) +signal.signal(signal.SIGUSR2, _resume) + +def open_tap(vif_name, vif_type, pi): + flags = 0 + flags |= vif_type + + if not pi: + flags |= IFF_NO_PI + + fd = os.open("/dev/net/tun", os.O_RDWR) + + err = fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", vif_name, flags)) + if err < 0: + os.close(fd) + raise RuntimeError("Could not configure device %s" % vif_name) + + return fd + +def get_options(): + usage = ("usage: %prog -N -t -p " + "-b -c -k -q " + "-l -r -H " + "-R ") + + parser = OptionParser(usage = usage) + + parser.add_option("-N", "--vif-name", dest="vif_name", + help = "The name of the virtual interface", + type="str") + + parser.add_option("-t", "--vif-type", dest="vif_type", + help = "Virtual interface type. Either IFF_TAP or IFF_TUN. " + "Defaults to IFF_TAP. ", + default = IFF_TAP, + type="str") + + parser.add_option("-n", "--pi", dest="pi", + action="store_true", + default = False, + help="Enable PI header") + + parser.add_option("-b", "--bwlimit", dest="bwlimit", + help = "Specifies the interface's emulated bandwidth in bytes ", + default = None, type="int") + + parser.add_option("-q", "--txqueuelen", dest="txqueuelen", + help = "Specifies the interface's transmission queue length. ", + default = 1000, type="int") + + parser.add_option("-c", "--cipher", dest="cipher", + help = "Cipher to encript communication. " + "One of PLAIN, AES, Blowfish, DES, DES3. ", + default = None, type="str") + + parser.add_option("-k", "--cipher-key", dest="cipher_key", + help = "Specify a symmetric encryption key with which to protect " + "packets across the tunnel. python-crypto must be installed " + "on the system." , + default = None, type="str") + + parser.add_option("-l", "--local-port-file", dest="local_port_file", + help = "File where to store the local binded UDP port number ", + default = "local_port_file", type="str") + + parser.add_option("-r", "--remote-port-file", dest="remote_port_file", + help = "File where to read the remote UDP port number to connect to", + default = "remote_port_file", type="str") + + parser.add_option("-H", "--remote-host", dest="remote_host", + help = "Remote host IP", + default = "remote_host", type="str") + + parser.add_option("-R", "--ret-file", dest="ret_file", + help = "File where to store return code (success of connection) ", + default = "ret_file", type="str") + + (options, args) = parser.parse_args() + + if options.vif_type == "IFF_TUN": + vif_type = IFF_TUN + + return ( options.vif_name, vif_type, options.pi, + options.local_port_file, options.remote_port_file, + options.remote_host, options.ret_file, options.bwlimit, + options.cipher, options.cipher_key, options.txqueuelen ) + +if __name__ == '__main__': + + ( vif_name, vif_type, pi, local_port_file, remote_port_file, + remote_host, ret_file, bwlimit, cipher, cipher_key, txqueuelen + ) = get_options() + + # Get the file descriptor of the TAP device from the process + # that created it + fd = open_tap(vif_name, vif_type, pi) + + # Create a local socket to stablish the tunnel connection + hostaddr = socket.gethostbyname(socket.gethostname()) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + sock.bind((hostaddr, 0)) + (local_host, local_port) = sock.getsockname() + + # Save local port information to file + f = open(local_port_file, 'w') + f.write("%d\n" % local_port) + f.close() + + # Wait until remote port information is available + while not os.path.exists(remote_port_file): + time.sleep(2) + + remote_port = '' + # Read remote port from file + # Try until something is read... + # xxx: There seems to be a weird behavior where + # even if the file exists and had the port number, + # the read operation returns empty string! + # Maybe a race condition? + for i in xrange(10): + f = open(remote_port_file, 'r') + remote_port = f.read() + f.close() + + if remote_port: + break + + time.sleep(2) + + remote_port = remote_port.strip() + remote_port = int(remote_port) + + # Connect local socket to remote port + sock.connect((remote_host, remote_port)) + remote = os.fdopen(sock.fileno(), 'r+b', 0) + + # TODO: Test connectivity! + + # Create a ret_file to indicate success + f = open(ret_file, 'w') + f.write("0") + f.close() + + # Establish tunnel + tunchannel.tun_fwd(tun, remote, + with_pi = True, # Planetlab TAP devices add PI headers + ether_mode = (vif_type == IFF_TAP), + udp = True, + cipher_key = cipher_key, + cipher = cipher, + TERMINATE = TERMINATE, + SUSPEND = SUSPEND, + tunqueue = txqueuelen, + tunkqueue = 500, + bwlimit = bwlimit + ) + diff --git a/src/nepi/resources/linux/tap.py b/src/nepi/resources/linux/tap.py index ee977ef3..f2e42a68 100644 --- a/src/nepi/resources/linux/tap.py +++ b/src/nepi/resources/linux/tap.py @@ -76,6 +76,10 @@ class LinuxTap(LinuxApplication): "Public IP of remote endpoint for GRE tunnel", flags = Flags.Design) + pi = Attribute("pi", "Add PI (protocol information) header", + default = False, + type = Types.Bool) + tear_down = Attribute("tearDown", "Bash script to be executed before releasing the resource", flags = Flags.Design) @@ -90,12 +94,14 @@ class LinuxTap(LinuxApplication): cls._register_attribute(txqueuelen) cls._register_attribute(gre_key) cls._register_attribute(gre_remote) + cls._register_attribute(pi) cls._register_attribute(tear_down) def __init__(self, ec, guid): super(LinuxTap, self).__init__(ec, guid) self._home = "tap-%s" % self.guid self._gre_enabled = False + self._tunnel_mode = False @property def node(self): @@ -112,7 +118,37 @@ class LinuxTap(LinuxApplication): return self._gre_enabled + @property + def tunnel_mode(self): + if not self._tunnel_mode: + from nepi.resources.linux.tunnel import LinuxTunnel + tunnel = self.get_connected(LinuxTunnel.get_rtype()) + if tunnel: self._tunnel_mode = True + + return self._tunnel_mode + def upload_sources(self): + scripts = [] + + # udp-connect python script + udp_connect = os.path.join(os.path.dirname(__file__), "scripts", + "linux-udp-connect.py") + + scripts.append(udp_connect) + + # tunnel creation python script + tunchannel = os.path.join(os.path.dirname(__file__), "scripts", + "tunchannel.py") + + scripts.append(tunchannel) + + # Upload scripts + scripts = ";".join(scripts) + + self.node.upload(scripts, + os.path.join(self.node.src_dir), + overwrite = False) + # upload stop.sh script stop_command = self.replace_paths(self._stop_command) @@ -127,7 +163,7 @@ class LinuxTap(LinuxApplication): def upload_start_command(self): # If GRE mode is enabled, TAP creation is delayed until the # tunnel is established - if not self.gre_enabled: + if not self.tunnel_mode: # We want to make sure the device is up and running # before the deploy is over, so we execute the # start script now and wait until it finishes. @@ -225,7 +261,7 @@ class LinuxTap(LinuxApplication): # invoke connect script cmd = "bash %s" % shfile - (out, err), proc = self.node.run(cmd, connection_run_home) + (out, err), proc = self.node.run(cmd, connection_run_home) # check if execution errors occurred msg = " Failed to connect endpoints " @@ -249,6 +285,105 @@ class LinuxTap(LinuxApplication): return True + ## XXX: NOT REALLY WORKING YET! + def udp_connect(self, remote_endpoint, connection_app_home, + connection_run_home, cipher, cipher_key, bwlimit, txqueuelen): + udp_connect_command = self._udp_connect_command( + remote_endpoint, connection_run_home, + cipher, cipher_key, bwlimit, txqueuelen) + + # upload command to connect.sh script + shfile = os.path.join(connection_app_home, "udp-connect.sh") + self.node.upload(udp_connect_command, + shfile, + text = True, + overwrite = False) + + # invoke connect script + cmd = "bash %s" % shfile + (out, err), proc = self.node.run(cmd, connection_run_home) + + # check if execution errors occurred + msg = "Failed to connect endpoints " + + if proc.poll(): + self.error(msg, out, err) + raise RuntimeError, msg + + # Wait for pid file to be generated + pid, ppid = self.node.wait_pid(connection_run_home) + + # If the process is not running, check for error information + # on the remote machine + if not pid or not ppid: + (out, err), proc = self.node.check_errors(connection_run_home) + # Out is what was written in the stderr file + if err: + msg = " Failed to start command '%s' " % command + self.error(msg, out, err) + raise RuntimeError, msg + + return pid, ppid + + def _udp_connect_command(self, remote_endpoint, connection_run_home, + cipher, cipher_key, bwlimit, txqueuelen): + + # Set the remote endpoint + self.set("pointopoint", remote_endpoint.get("ip4")) + + # Planetlab TAPs always use PI headers + from nepi.resources.planetlab.tap import PlanetlabTap + if self.is_rm_instance(PlanetlabTap.get_rtype()): + self.set("pi", True) + + remote_ip = socket.gethostbyname( + remote_endpoint.node.get("hostname")) + + local_port_file = os.path.join(connection_run_home, + "local_port") + + remote_port_file = os.path.join(connection_run_home, + "remote_port") + + ret_file = os.path.join(connection_run_home, + "ret_file") + + # Generate UDP connect command + # Use the start command to configure TAP with peer info + start_command = self._start_command + + command = ["( "] + command.append(start_command) + + # Use pl-vid-udp-connect.py to stablish the tunnel between endpoints + command.append(") & (") + command.append("sudo -S") + command.append("PYTHONPATH=$PYTHONPATH:${SRC}") + command.append("python ${SRC}/linux-udp-connect.py") + command.append("-N %s" % self.get("deviceName")) + command.append("-t %s" % self.vif_type) + if self.get("pi"): + command.append("-p") + command.append("-l %s " % local_port_file) + command.append("-r %s " % remote_port_file) + command.append("-H %s " % remote_ip) + command.append("-R %s " % ret_file) + if cipher: + command.append("-c %s " % cipher) + if cipher_key: + command.append("-k %s " % cipher_key) + if txqueuelen: + command.append("-q %s " % txqueuelen) + if bwlimit: + command.append("-b %s " % bwlimit) + + command.append(")") + + command = " ".join(command) + command = self.replace_paths(command) + + return command + def _gre_connect_command(self, remote_endpoint, connection_run_home): # Set the remote endpoint self.set("pointopoint", remote_endpoint.get("ip4")) @@ -270,14 +405,31 @@ class LinuxTap(LinuxApplication): @property def _start_command(self): command = [] - command.append("sudo -S ip tuntap add %s mode tap" % self.get("deviceName")) - command.append("sudo -S ip link set %s up" % self.get("deviceName")) - command.append("sudo -S ip addr add %s/%d dev %s" % ( - self.get("ip4"), - self.get("prefix4"), - self.get("deviceName"), - )) - return ";".join(command) + if not self.gre_enabled: + # Make sure to clean TAP if it existed + stop_command = self._stop_command + + start_command = [] + start_command.append("sudo -S ip tuntap add %s mode %s %s" % ( + self.get("deviceName"), + self.vif_prefix, + "pi" if self.get("pi") else "")) + start_command.append("sudo -S ip link set %s up" % self.get("deviceName")) + start_command.append("sudo -S ip addr add %s/%d dev %s" % ( + self.get("ip4"), + self.get("prefix4"), + self.get("deviceName"), + )) + + start_command = ";".join(start_command) + + command.append("(") + command.append(stop_command) + command.append(") ; (") + command.append(start_command) + command.append(")") + + return " ".join(command) @property def _stop_command(self): diff --git a/test/resources/linux/gretunnel.py b/test/resources/linux/gretunnel.py index ea57542c..cb557604 100755 --- a/test/resources/linux/gretunnel.py +++ b/test/resources/linux/gretunnel.py @@ -29,9 +29,9 @@ import unittest class LinuxGRETunnelTestCase(unittest.TestCase): def setUp(self): self.host1 = "roseval.pl.sophia.inria.fr" - self.host2 = "truckers.pl.sophia.inria.fr" + self.host2 = "138.96.118.11" self.user1 = "inria_nepi" - self.user2 = "aquereil" + self.user2 = "omflab" self.identity = "%s/.ssh/id_rsa_planetlab" % (os.environ['HOME']) self.netblock = "192.168.1" @@ -50,7 +50,7 @@ class LinuxGRETunnelTestCase(unittest.TestCase): tap1 = ec.register_resource("LinuxTap") ec.set(tap1, "ip4", "%s.1" % self.netblock) - ec.set(tap1, "prefix4", 24) + ec.set(tap1, "prefix4", 32) ec.register_connection(tap1, node1) node2 = ec.register_resource("LinuxNode") @@ -62,7 +62,7 @@ class LinuxGRETunnelTestCase(unittest.TestCase): tap2 = ec.register_resource("LinuxTap") ec.set(tap2, "ip4", "%s.2" % self.netblock) - ec.set(tap2, "prefix4", 24) + ec.set(tap2, "prefix4", 32) ec.register_connection(tap2, node2) gretun = ec.register_resource("LinuxGRETunnel") @@ -105,7 +105,7 @@ class LinuxGRETunnelTestCase(unittest.TestCase): tun1 = ec.register_resource("LinuxTun") ec.set(tun1, "ip4", "%s.1" % self.netblock) - ec.set(tun1, "prefix4", 24) + ec.set(tun1, "prefix4", 32) ec.register_connection(tun1, node1) node2 = ec.register_resource("LinuxNode") @@ -117,7 +117,7 @@ class LinuxGRETunnelTestCase(unittest.TestCase): tun2 = ec.register_resource("LinuxTun") ec.set(tun2, "ip4", "%s.2" % self.netblock) - ec.set(tun2, "prefix4", 24) + ec.set(tun2, "prefix4", 32) ec.register_connection(tun2, node2) udptun = ec.register_resource("LinuxGRETunnel") diff --git a/test/resources/linux/udptunnel.py b/test/resources/linux/udptunnel.py new file mode 100755 index 00000000..22f4f3d6 --- /dev/null +++ b/test/resources/linux/udptunnel.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# NEPI, a framework to manage network experiments +# Copyright (C) 2013 INRIA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author: Alina Quereilhac + +from nepi.execution.ec import ExperimentController + +from test_utils import skipIfAnyNotAliveWithIdentity + +import os +import time +import unittest + +class LinuxUdpTunnelTestCase(unittest.TestCase): + def setUp(self): + self.host1 = "roseval.pl.sophia.inria.fr" + self.host2 = "138.96.118.11" + self.user1 = "inria_nepi" + self.user2 = "omflab" + self.identity = "%s/.ssh/id_rsa_planetlab" % (os.environ['HOME']) + self.netblock = "192.168.1" + + @skipIfAnyNotAliveWithIdentity + def t_tap_udp_tunnel(self, user1, host1, identity1, user2, host2, + identity2): + + ec = ExperimentController(exp_id = "test-tap-udp-tunnel") + + node1 = ec.register_resource("LinuxNode") + ec.set(node1, "hostname", host1) + ec.set(node1, "username", user1) + ec.set(node1, "identity", identity1) + ec.set(node1, "cleanHome", True) + ec.set(node1, "cleanProcesses", True) + + tap1 = ec.register_resource("LinuxTap") + ec.set(tap1, "ip4", "%s.1" % self.netblock) + ec.set(tap1, "prefix4", 32) + ec.register_connection(tap1, node1) + + node2 = ec.register_resource("LinuxNode") + ec.set(node2, "hostname", host2) + ec.set(node2, "username", user2) + ec.set(node2, "identity", identity2) + ec.set(node2, "cleanHome", True) + ec.set(node2, "cleanProcesses", True) + + tap2 = ec.register_resource("LinuxTap") + ec.set(tap2, "ip4", "%s.2" % self.netblock) + ec.set(tap2, "prefix4", 32) + ec.register_connection(tap2, node2) + + udptun = ec.register_resource("LinuxUdpTunnel") + ec.register_connection(tap1, udptun) + ec.register_connection(tap2, udptun) + + app = ec.register_resource("LinuxApplication") + cmd = "ping -c3 %s.2" % self.netblock + ec.set(app, "command", cmd) + ec.register_connection(app, node1) + + ec.deploy() + + ec.wait_finished(app) + + ping = ec.trace(app, 'stdout') + expected = """3 packets transmitted, 3 received, 0% packet loss""" + self.assertTrue(ping.find(expected) > -1) + + vif_name = ec.get(tap1, "deviceName") + self.assertTrue(vif_name.startswith("tap")) + + vif_name = ec.get(tap2, "deviceName") + self.assertTrue(vif_name.startswith("tap")) + + ec.shutdown() + + @skipIfAnyNotAliveWithIdentity + def t_tun_udp_tunnel(self, user1, host1, identity1, user2, host2, identity2): + + ec = ExperimentController(exp_id = "test-tun-udp-tunnel") + + node1 = ec.register_resource("LinuxNode") + ec.set(node1, "hostname", host1) + ec.set(node1, "username", user1) + ec.set(node1, "identity", identity1) + ec.set(node1, "cleanHome", True) + ec.set(node1, "cleanProcesses", True) + + tun1 = ec.register_resource("LinuxTun") + ec.set(tun1, "ip4", "%s.1" % self.netblock) + ec.set(tun1, "prefix4", 32) + ec.register_connection(tun1, node1) + + node2 = ec.register_resource("LinuxNode") + ec.set(node2, "hostname", host2) + ec.set(node2, "username", user2) + ec.set(node2, "identity", identity2) + ec.set(node2, "cleanHome", True) + ec.set(node2, "cleanProcesses", True) + + tun2 = ec.register_resource("LinuxTun") + ec.set(tun2, "ip4", "%s.2" % self.netblock) + ec.set(tun2, "prefix4", 32) + ec.register_connection(tun2, node2) + + udptun = ec.register_resource("LinuxUdpTunnel") + ec.register_connection(tun1, udptun) + ec.register_connection(tun2, udptun) + + app = ec.register_resource("LinuxApplication") + cmd = "ping -c3 %s.2" % self.netblock + ec.set(app, "command", cmd) + ec.register_connection(app, node1) + + ec.deploy() + + ec.wait_finished(app) + + ping = ec.trace(app, 'stdout') + expected = """3 packets transmitted, 3 received, 0% packet loss""" + self.assertTrue(ping.find(expected) > -1) + + vif_name = ec.get(tun1, "deviceName") + self.assertTrue(vif_name.startswith("tun")) + + vif_name = ec.get(tun2, "deviceName") + self.assertTrue(vif_name.startswith("tun")) + + ec.shutdown() + + def ztest_tap_udp_tunnel(self): + self.t_tap_udp_tunnel(self.user1, self.host1, self.identity, + self.user2, self.host2, self.identity) + + def test_tun_udp_tunnel(self): + self.t_tun_udp_tunnel(self.user1, self.host1, self.identity, + self.user2, self.host2, self.identity) + +if __name__ == '__main__': + unittest.main() + diff --git a/test/resources/planetlab/gretunnel.py b/test/resources/planetlab/gretunnel.py index 33917ab3..6548ed27 100755 --- a/test/resources/planetlab/gretunnel.py +++ b/test/resources/planetlab/gretunnel.py @@ -183,7 +183,9 @@ class PlanetLabGRETunnelTestCase(unittest.TestCase): ec.register_connection(tun2, gretun) app = ec.register_resource("LinuxApplication") - cmd = "ping -c3 %s.2" % self.netblock + # It seems the hybrid tunnel takes some time to setup... we add a sleep 5 + # XXX: Debug this to see if it can be fixed on the RMs + cmd = "sleep 5; ping -c3 %s.2" % self.netblock ec.set(app, "command", cmd) ec.register_connection(app, node1) diff --git a/test/resources/planetlab/udptunnel.py b/test/resources/planetlab/udptunnel.py index 36915ac0..a74acee6 100755 --- a/test/resources/planetlab/udptunnel.py +++ b/test/resources/planetlab/udptunnel.py @@ -26,7 +26,7 @@ import os import time import unittest -class UdpTunnelTestCase(unittest.TestCase): +class PlanetlabUdpTunnelTestCase(unittest.TestCase): def setUp(self): #self.host1 = "nepi2.pl.sophia.inria.fr" #self.host2 = "nepi5.pl.sophia.inria.fr"