PL Point-to-point link support (TUNs automatically set their interfaces for P2P,...
[nepi.git] / src / nepi / testbeds / planetlab / scripts / tun_connect.py
1 import sys
2
3 import socket
4 import fcntl
5 import os
6 import os.path
7 import select
8 import signal
9
10 import struct
11 import ctypes
12 import optparse
13 import threading
14 import subprocess
15 import re
16 import functools
17 import time
18 import base64
19
20 import tunchannel
21
22 tun_name = 'tun0'
23 tun_path = '/dev/net/tun'
24 hostaddr = socket.gethostbyname(socket.gethostname())
25
26 usage = "usage: %prog [options] <remote-endpoint>"
27
28 parser = optparse.OptionParser(usage=usage)
29
30 parser.add_option(
31     "-i", "--iface", dest="tun_name", metavar="DEVICE",
32     default = "tun0",
33     help = "TUN/TAP interface to tap into")
34 parser.add_option(
35     "-d", "--tun-path", dest="tun_path", metavar="PATH",
36     default = "/dev/net/tun",
37     help = "TUN/TAP device file path or file descriptor number")
38 parser.add_option(
39     "-p", "--port", dest="port", metavar="PORT", type="int",
40     default = 15000,
41     help = "Peering TCP port to connect or listen to.")
42 parser.add_option(
43     "--pass-fd", dest="pass_fd", metavar="UNIX_SOCKET",
44     default = None,
45     help = "Path to a unix-domain socket to pass the TUN file descriptor to. "
46            "If given, all other connectivity options are ignored, tun_connect will "
47            "simply wait to be killed after passing the file descriptor, and it will be "
48            "the receiver's responsability to handle the tunneling.")
49
50 parser.add_option(
51     "-m", "--mode", dest="mode", metavar="MODE",
52     default = "none",
53     help = 
54         "Set mode. One of none, tun, tap, pl-tun, pl-tap. In any mode except none, a TUN/TAP will be created "
55         "by using the proper interface (tunctl for tun/tap, /vsys/fd_tuntap.control for pl-tun/pl-tap), "
56         "and it will be brought up (with ifconfig for tun/tap, with /vsys/vif_up for pl-tun/pl-tap). You have "
57         "to specify an VIF_ADDRESS and VIF_MASK in any case (except for none).")
58 parser.add_option(
59     "-A", "--vif-address", dest="vif_addr", metavar="VIF_ADDRESS",
60     default = None,
61     help = 
62         "See mode. This specifies the VIF_ADDRESS, "
63         "the IP address of the virtual interface.")
64 parser.add_option(
65     "-M", "--vif-mask", dest="vif_mask", type="int", metavar="VIF_MASK", 
66     default = None,
67     help = 
68         "See mode. This specifies the VIF_MASK, "
69         "a number indicating the network type (ie: 24 for a C-class network).")
70 parser.add_option(
71     "-S", "--vif-snat", dest="vif_snat", 
72     action = "store_true",
73     default = False,
74     help = "See mode. This specifies whether SNAT will be enabled for the virtual interface. " )
75 parser.add_option(
76     "-P", "--vif-pointopoint", dest="vif_pointopoint",  metavar="DST_ADDR",
77     default = None,
78     help = 
79         "See mode. This specifies the remote endpoint's virtual address, "
80         "for point-to-point routing configuration. "
81         "Not supported by PlanetLab" )
82 parser.add_option(
83     "-Q", "--vif-txqueuelen", dest="vif_txqueuelen", metavar="SIZE", type="int",
84     default = None,
85     help = 
86         "See mode. This specifies the interface's transmission queue length. " )
87 parser.add_option(
88     "-u", "--udp", dest="udp", metavar="PORT", type="int",
89     default = None,
90     help = 
91         "Bind to the specified UDP port locally, and send UDP datagrams to the "
92         "remote endpoint, creating a tunnel through UDP rather than TCP." )
93 parser.add_option(
94     "-k", "--key", dest="cipher_key", metavar="KEY",
95     default = None,
96     help = 
97         "Specify a symmetric encryption key with which to protect packets across "
98         "the tunnel. python-crypto must be installed on the system." )
99
100 (options, remaining_args) = parser.parse_args(sys.argv[1:])
101
102
103 ETH_P_ALL = 0x00000003
104 ETH_P_IP = 0x00000800
105 TUNSETIFF = 0x400454ca
106 IFF_NO_PI = 0x00001000
107 IFF_TAP = 0x00000002
108 IFF_TUN = 0x00000001
109 IFF_VNET_HDR = 0x00004000
110 TUN_PKT_STRIP = 0x00000001
111 IFHWADDRLEN = 0x00000006
112 IFNAMSIZ = 0x00000010
113 IFREQ_SZ = 0x00000028
114 FIONREAD = 0x0000541b
115
116 def ifnam(x):
117     return x+'\x00'*(IFNAMSIZ-len(x))
118
119 def ifreq(iface, flags):
120     # ifreq contains:
121     #   char[IFNAMSIZ] : interface name
122     #   short : flags
123     #   <padding>
124     ifreq = ifnam(iface)+struct.pack("H",flags);
125     ifreq += '\x00' * (len(ifreq)-IFREQ_SZ)
126     return ifreq
127
128 def tunopen(tun_path, tun_name):
129     if tun_path.isdigit():
130         # open TUN fd
131         print >>sys.stderr, "Using tun:", tun_name, "fd", tun_path
132         tun = os.fdopen(int(tun_path), 'r+b', 0)
133     else:
134         # open TUN path
135         print >>sys.stderr, "Using tun:", tun_name, "at", tun_path
136         tun = open(tun_path, 'r+b', 0)
137
138         # bind file descriptor to the interface
139         fcntl.ioctl(tun.fileno(), TUNSETIFF, ifreq(tun_name, IFF_NO_PI|IFF_TUN))
140     
141     return tun
142
143 def tunclose(tun_path, tun_name, tun):
144     if tun_path.isdigit():
145         # close TUN fd
146         os.close(int(tun_path))
147         tun.close()
148     else:
149         # close TUN object
150         tun.close()
151
152 def tuntap_alloc(kind, tun_path, tun_name):
153     args = ["tunctl"]
154     if kind == "tun":
155         args.append("-n")
156     if tun_name:
157         args.append("-t")
158         args.append(tun_name)
159     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
160     out,err = proc.communicate()
161     if proc.wait():
162         raise RuntimeError, "Could not allocate %s device" % (kind,)
163         
164     match = re.search(r"Set '(?P<dev>(?:tun|tap)[0-9]*)' persistent and owned by .*", out, re.I)
165     if not match:
166         raise RuntimeError, "Could not allocate %s device - tunctl said: %s" % (kind, out)
167     
168     tun_name = match.group("dev")
169     print >>sys.stderr, "Allocated %s device: %s" % (kind, tun_name)
170     
171     return tun_path, tun_name
172
173 def tuntap_dealloc(tun_path, tun_name):
174     args = ["tunctl", "-d", tun_name]
175     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
176     out,err = proc.communicate()
177     if proc.wait():
178         print >> sys.stderr, "WARNING: error deallocating %s device" % (tun_name,)
179
180 def nmask_to_dot_notation(mask):
181     mask = hex(((1 << mask) - 1) << (32 - mask)) # 24 -> 0xFFFFFF00
182     mask = mask[2:] # strip 0x
183     mask = mask.decode("hex") # to bytes
184     mask = '.'.join(map(str,map(ord,mask))) # to 255.255.255.0
185     return mask
186
187 def vif_start(tun_path, tun_name):
188     args = ["ifconfig", tun_name, options.vif_addr, 
189             "netmask", nmask_to_dot_notation(options.vif_mask),
190             "-arp" ]
191     if options.vif_pointopoint:
192         args.extend(["pointopoint",options.vif_pointopoint])
193     if options.vif_txqueuelen is not None:
194         args.extend(["txqueuelen",str(options.vif_txqueuelen)])
195     args.append("up")
196     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
197     out,err = proc.communicate()
198     if proc.wait():
199         raise RuntimeError, "Error starting virtual interface"
200     
201     if options.vif_snat:
202         # set up SNAT using iptables
203         # TODO: stop vif on error. 
204         #   Not so necessary since deallocating the tun/tap device
205         #   will forcibly stop it, but it would be tidier
206         args = [ "iptables", "-t", "nat", "-A", "POSTROUTING", 
207                  "-s", "%s/%d" % (options.vif_addr, options.vif_mask),
208                  "-j", "SNAT",
209                  "--to-source", hostaddr, "--random" ]
210         proc = subprocess.Popen(args, stdout=subprocess.PIPE)
211         out,err = proc.communicate()
212         if proc.wait():
213             raise RuntimeError, "Error setting up SNAT"
214
215 def vif_stop(tun_path, tun_name):
216     if options.vif_snat:
217         # set up SNAT using iptables
218         args = [ "iptables", "-t", "nat", "-D", "POSTROUTING", 
219                  "-s", "%s/%d" % (options.vif_addr, options.vif_mask),
220                  "-j", "SNAT",
221                  "--to-source", hostaddr, "--random" ]
222         proc = subprocess.Popen(args, stdout=subprocess.PIPE)
223         out,err = proc.communicate()
224     
225     args = ["ifconfig", tun_name, "down"]
226     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
227     out,err = proc.communicate()
228     if proc.wait():
229         print >>sys.stderr, "WARNING: error stopping virtual interface"
230     
231     
232 def pl_tuntap_alloc(kind, tun_path, tun_name):
233     tunalloc_so = ctypes.cdll.LoadLibrary("./tunalloc.so")
234     c_tun_name = ctypes.c_char_p("\x00"*IFNAMSIZ) # the string will be mutated!
235     kind = {"tun":IFF_TUN,
236             "tap":IFF_TAP}[kind]
237     fd = tunalloc_so.tun_alloc(kind, c_tun_name)
238     name = c_tun_name.value
239     return str(fd), name
240
241 def pl_vif_start(tun_path, tun_name):
242     stdin = open("/vsys/vif_up.in","w")
243     stdout = open("/vsys/vif_up.out","r")
244     stdin.write(tun_name+"\n")
245     stdin.write(options.vif_addr+"\n")
246     stdin.write(str(options.vif_mask)+"\n")
247     if options.vif_snat:
248         stdin.write("snat=1\n")
249     if options.vif_pointopoint:
250         stdin.write("pointopoint=%s\n" % (options.vif_pointopoint,))
251     if options.vif_txqueuelen is not None:
252         stdin.write("txqueuelen=%d\n" % (options.vif_txqueuelen,))
253     stdin.close()
254     out = stdout.read()
255     stdout.close()
256     if out.strip():
257         print >>sys.stderr, out
258
259 def pl_vif_stop(tun_path, tun_name):
260     stdin = open("/vsys/vif_down.in","w")
261     stdout = open("/vsys/vif_down.out","r")
262     stdin.write(tun_name+"\n")
263     stdin.close()
264     out = stdout.read()
265     stdout.close()
266     if out.strip():
267         print >>sys.stderr, out
268
269
270 def tun_fwd(tun, remote):
271     global TERMINATE
272     
273     # in PL mode, we cannot strip PI structs
274     # so we'll have to handle them
275     tunchannel.tun_fwd(tun, remote,
276         with_pi = options.mode.startswith('pl-'),
277         ether_mode = tun_name.startswith('tap'),
278         cipher_key = options.cipher_key,
279         udp = options.udp,
280         TERMINATE = TERMINATE)
281
282
283
284 nop = lambda tun_path, tun_name : (tun_path, tun_name)
285 MODEINFO = {
286     'none' : dict(alloc=nop,
287                   tunopen=tunopen, tunclose=tunclose,
288                   dealloc=nop,
289                   start=nop,
290                   stop=nop),
291     'tun'  : dict(alloc=functools.partial(tuntap_alloc, "tun"),
292                   tunopen=tunopen, tunclose=tunclose,
293                   dealloc=tuntap_dealloc,
294                   start=vif_start,
295                   stop=vif_stop),
296     'tap'  : dict(alloc=functools.partial(tuntap_alloc, "tap"),
297                   tunopen=tunopen, tunclose=tunclose,
298                   dealloc=tuntap_dealloc,
299                   start=vif_start,
300                   stop=vif_stop),
301     'pl-tun'  : dict(alloc=functools.partial(pl_tuntap_alloc, "tun"),
302                   tunopen=tunopen, tunclose=tunclose,
303                   dealloc=nop,
304                   start=pl_vif_start,
305                   stop=pl_vif_stop),
306     'pl-tap'  : dict(alloc=functools.partial(pl_tuntap_alloc, "tap"),
307                   tunopen=tunopen, tunclose=tunclose,
308                   dealloc=nop,
309                   start=pl_vif_start,
310                   stop=pl_vif_stop),
311 }
312     
313 tun_path = options.tun_path
314 tun_name = options.tun_name
315
316 modeinfo = MODEINFO[options.mode]
317
318 # be careful to roll back stuff on exceptions
319 tun_path, tun_name = modeinfo['alloc'](tun_path, tun_name)
320 try:
321     modeinfo['start'](tun_path, tun_name)
322     try:
323         tun = modeinfo['tunopen'](tun_path, tun_name)
324     except:
325         modeinfo['stop'](tun_path, tun_name)
326         raise
327 except:
328     modeinfo['dealloc'](tun_path, tun_name)
329     raise
330
331
332 # Trak SIGTERM, and set global termination flag instead of dying
333 TERMINATE = []
334 def _finalize(sig,frame):
335     global TERMINATE
336     TERMINATE.append(None)
337 signal.signal(signal.SIGTERM, _finalize)
338
339 try:
340     tcpdump = None
341     
342     if options.pass_fd:
343         if options.pass_fd.startswith("base64:"):
344             options.pass_fd = base64.b64decode(
345                 options.pass_fd[len("base64:"):])
346             options.pass_fd = os.path.expandvars(options.pass_fd)
347         
348         print >>sys.stderr, "Sending FD to: %r" % (options.pass_fd,)
349         
350         # send FD to whoever wants it
351         import passfd
352         
353         sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
354         for i in xrange(30):
355             try:
356                 sock.connect(options.pass_fd)
357                 break
358             except socket.error:
359                 # wait a while, retry
360                 print >>sys.stderr, "Could not connect. Retrying in a sec..."
361                 time.sleep(1)
362         else:
363             sock.connect(options.pass_fd)
364         passfd.sendfd(sock, tun.fileno(), '0')
365         
366         # Launch a tcpdump subprocess, to capture and dump packets,
367         # we will not be able to capture them ourselves.
368         # Make sure to catch sigterm and kill the tcpdump as well
369         tcpdump = subprocess.Popen(
370             ["tcpdump","-l","-n","-i",tun_name])
371         
372         # just wait forever
373         def tun_fwd(tun, remote):
374             while not TERMINATE:
375                 time.sleep(1)
376         remote = None
377     elif options.udp:
378         # connect to remote endpoint
379         if remaining_args and not remaining_args[0].startswith('-'):
380             print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.udp)
381             print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
382             rsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
383             rsock.bind((hostaddr,options.udp))
384             rsock.connect((remaining_args[0],options.port))
385         else:
386             print >>sys.stderr, "Error: need a remote endpoint in UDP mode"
387             raise AssertionError, "Error: need a remote endpoint in UDP mode"
388         remote = os.fdopen(rsock.fileno(), 'r+b', 0)
389     else:
390         # connect to remote endpoint
391         if remaining_args and not remaining_args[0].startswith('-'):
392             print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
393             rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
394             for i in xrange(30):
395                 try:
396                     rsock.connect((remaining_args[0],options.port))
397                     break
398                 except socket.error:
399                     # wait a while, retry
400                     print >>sys.stderr, "Could not connect. Retrying in a sec..."
401                     time.sleep(1)
402             else:
403                 rsock.connect((remaining_args[0],options.port))
404         else:
405             print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.port)
406             lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
407             lsock.bind((hostaddr,options.port))
408             lsock.listen(1)
409             rsock,raddr = lsock.accept()
410         remote = os.fdopen(rsock.fileno(), 'r+b', 0)
411
412     print >>sys.stderr, "Connected"
413
414     tun_fwd(tun, remote)
415
416     if tcpdump:
417         os.kill(tcpdump.pid, signal.SIGTERM)
418         tcpdump.wait()
419 finally:
420     try:
421         print >>sys.stderr, "Shutting down..."
422     except:
423         # In case sys.stderr is broken
424         pass
425     
426     # tidy shutdown in every case - swallow exceptions
427     try:
428         modeinfo['stop'](tun_path, tun_name)
429     except:
430         pass
431
432     try:
433         modeinfo['tunclose'](tun_path, tun_name, tun)
434     except:
435         pass
436         
437     try:
438         modeinfo['dealloc'](tun_path, tun_name)
439     except:
440         pass
441     
442     print >>sys.stderr, "TERMINATED GRACEFULLY"
443