A series of synchronization fixes:
[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 parser.add_option(
100     "-N", "--no-capture", dest="no_capture", 
101     action = "store_true",
102     default = False,
103     help = "If specified, packets won't be logged to standard error "
104            "(default is to log them to standard error). " )
105
106 (options, remaining_args) = parser.parse_args(sys.argv[1:])
107
108
109 ETH_P_ALL = 0x00000003
110 ETH_P_IP = 0x00000800
111 TUNSETIFF = 0x400454ca
112 IFF_NO_PI = 0x00001000
113 IFF_TAP = 0x00000002
114 IFF_TUN = 0x00000001
115 IFF_VNET_HDR = 0x00004000
116 TUN_PKT_STRIP = 0x00000001
117 IFHWADDRLEN = 0x00000006
118 IFNAMSIZ = 0x00000010
119 IFREQ_SZ = 0x00000028
120 FIONREAD = 0x0000541b
121
122 def ifnam(x):
123     return x+'\x00'*(IFNAMSIZ-len(x))
124
125 def ifreq(iface, flags):
126     # ifreq contains:
127     #   char[IFNAMSIZ] : interface name
128     #   short : flags
129     #   <padding>
130     ifreq = ifnam(iface)+struct.pack("H",flags);
131     ifreq += '\x00' * (len(ifreq)-IFREQ_SZ)
132     return ifreq
133
134 def tunopen(tun_path, tun_name):
135     if tun_path.isdigit():
136         # open TUN fd
137         print >>sys.stderr, "Using tun:", tun_name, "fd", tun_path
138         tun = os.fdopen(int(tun_path), 'r+b', 0)
139     else:
140         # open TUN path
141         print >>sys.stderr, "Using tun:", tun_name, "at", tun_path
142         tun = open(tun_path, 'r+b', 0)
143
144         # bind file descriptor to the interface
145         fcntl.ioctl(tun.fileno(), TUNSETIFF, ifreq(tun_name, IFF_NO_PI|IFF_TUN))
146     
147     return tun
148
149 def tunclose(tun_path, tun_name, tun):
150     if tun_path.isdigit():
151         # close TUN fd
152         os.close(int(tun_path))
153         tun.close()
154     else:
155         # close TUN object
156         tun.close()
157
158 def tuntap_alloc(kind, tun_path, tun_name):
159     args = ["tunctl"]
160     if kind == "tun":
161         args.append("-n")
162     if tun_name:
163         args.append("-t")
164         args.append(tun_name)
165     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
166     out,err = proc.communicate()
167     if proc.wait():
168         raise RuntimeError, "Could not allocate %s device" % (kind,)
169         
170     match = re.search(r"Set '(?P<dev>(?:tun|tap)[0-9]*)' persistent and owned by .*", out, re.I)
171     if not match:
172         raise RuntimeError, "Could not allocate %s device - tunctl said: %s" % (kind, out)
173     
174     tun_name = match.group("dev")
175     print >>sys.stderr, "Allocated %s device: %s" % (kind, tun_name)
176     
177     return tun_path, tun_name
178
179 def tuntap_dealloc(tun_path, tun_name):
180     args = ["tunctl", "-d", tun_name]
181     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
182     out,err = proc.communicate()
183     if proc.wait():
184         print >> sys.stderr, "WARNING: error deallocating %s device" % (tun_name,)
185
186 def nmask_to_dot_notation(mask):
187     mask = hex(((1 << mask) - 1) << (32 - mask)) # 24 -> 0xFFFFFF00
188     mask = mask[2:] # strip 0x
189     mask = mask.decode("hex") # to bytes
190     mask = '.'.join(map(str,map(ord,mask))) # to 255.255.255.0
191     return mask
192
193 def vif_start(tun_path, tun_name):
194     args = ["ifconfig", tun_name, options.vif_addr, 
195             "netmask", nmask_to_dot_notation(options.vif_mask),
196             "-arp" ]
197     if options.vif_pointopoint:
198         args.extend(["pointopoint",options.vif_pointopoint])
199     if options.vif_txqueuelen is not None:
200         args.extend(["txqueuelen",str(options.vif_txqueuelen)])
201     args.append("up")
202     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
203     out,err = proc.communicate()
204     if proc.wait():
205         raise RuntimeError, "Error starting virtual interface"
206     
207     if options.vif_snat:
208         # set up SNAT using iptables
209         # TODO: stop vif on error. 
210         #   Not so necessary since deallocating the tun/tap device
211         #   will forcibly stop it, but it would be tidier
212         args = [ "iptables", "-t", "nat", "-A", "POSTROUTING", 
213                  "-s", "%s/%d" % (options.vif_addr, options.vif_mask),
214                  "-j", "SNAT",
215                  "--to-source", hostaddr, "--random" ]
216         proc = subprocess.Popen(args, stdout=subprocess.PIPE)
217         out,err = proc.communicate()
218         if proc.wait():
219             raise RuntimeError, "Error setting up SNAT"
220
221 def vif_stop(tun_path, tun_name):
222     if options.vif_snat:
223         # set up SNAT using iptables
224         args = [ "iptables", "-t", "nat", "-D", "POSTROUTING", 
225                  "-s", "%s/%d" % (options.vif_addr, options.vif_mask),
226                  "-j", "SNAT",
227                  "--to-source", hostaddr, "--random" ]
228         proc = subprocess.Popen(args, stdout=subprocess.PIPE)
229         out,err = proc.communicate()
230     
231     args = ["ifconfig", tun_name, "down"]
232     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
233     out,err = proc.communicate()
234     if proc.wait():
235         print >>sys.stderr, "WARNING: error stopping virtual interface"
236     
237     
238 def pl_tuntap_alloc(kind, tun_path, tun_name):
239     tunalloc_so = ctypes.cdll.LoadLibrary("./tunalloc.so")
240     c_tun_name = ctypes.c_char_p("\x00"*IFNAMSIZ) # the string will be mutated!
241     kind = {"tun":IFF_TUN,
242             "tap":IFF_TAP}[kind]
243     fd = tunalloc_so.tun_alloc(kind, c_tun_name)
244     name = c_tun_name.value
245     return str(fd), name
246
247 def pl_vif_start(tun_path, tun_name):
248     stdin = open("/vsys/vif_up.in","w")
249     stdout = open("/vsys/vif_up.out","r")
250     stdin.write(tun_name+"\n")
251     stdin.write(options.vif_addr+"\n")
252     stdin.write(str(options.vif_mask)+"\n")
253     if options.vif_snat:
254         stdin.write("snat=1\n")
255     if options.vif_pointopoint:
256         stdin.write("pointopoint=%s\n" % (options.vif_pointopoint,))
257     if options.vif_txqueuelen is not None:
258         stdin.write("txqueuelen=%d\n" % (options.vif_txqueuelen,))
259     stdin.close()
260     out = stdout.read()
261     stdout.close()
262     if out.strip():
263         print >>sys.stderr, out
264
265 def pl_vif_stop(tun_path, tun_name):
266     stdin = open("/vsys/vif_down.in","w")
267     stdout = open("/vsys/vif_down.out","r")
268     stdin.write(tun_name+"\n")
269     stdin.close()
270     out = stdout.read()
271     stdout.close()
272     if out.strip():
273         print >>sys.stderr, out
274
275
276 def tun_fwd(tun, remote):
277     global TERMINATE
278     
279     # in PL mode, we cannot strip PI structs
280     # so we'll have to handle them
281     tunchannel.tun_fwd(tun, remote,
282         with_pi = options.mode.startswith('pl-'),
283         ether_mode = tun_name.startswith('tap'),
284         cipher_key = options.cipher_key,
285         udp = options.udp,
286         TERMINATE = TERMINATE,
287         stderr = open("/dev/null","w") if options.no_capture 
288                  else sys.stderr 
289     )
290
291
292
293 nop = lambda tun_path, tun_name : (tun_path, tun_name)
294 MODEINFO = {
295     'none' : dict(alloc=nop,
296                   tunopen=tunopen, tunclose=tunclose,
297                   dealloc=nop,
298                   start=nop,
299                   stop=nop),
300     'tun'  : dict(alloc=functools.partial(tuntap_alloc, "tun"),
301                   tunopen=tunopen, tunclose=tunclose,
302                   dealloc=tuntap_dealloc,
303                   start=vif_start,
304                   stop=vif_stop),
305     'tap'  : dict(alloc=functools.partial(tuntap_alloc, "tap"),
306                   tunopen=tunopen, tunclose=tunclose,
307                   dealloc=tuntap_dealloc,
308                   start=vif_start,
309                   stop=vif_stop),
310     'pl-tun'  : dict(alloc=functools.partial(pl_tuntap_alloc, "tun"),
311                   tunopen=tunopen, tunclose=tunclose,
312                   dealloc=nop,
313                   start=pl_vif_start,
314                   stop=pl_vif_stop),
315     'pl-tap'  : dict(alloc=functools.partial(pl_tuntap_alloc, "tap"),
316                   tunopen=tunopen, tunclose=tunclose,
317                   dealloc=nop,
318                   start=pl_vif_start,
319                   stop=pl_vif_stop),
320 }
321     
322 tun_path = options.tun_path
323 tun_name = options.tun_name
324
325 modeinfo = MODEINFO[options.mode]
326
327 # be careful to roll back stuff on exceptions
328 tun_path, tun_name = modeinfo['alloc'](tun_path, tun_name)
329 try:
330     modeinfo['start'](tun_path, tun_name)
331     try:
332         tun = modeinfo['tunopen'](tun_path, tun_name)
333     except:
334         modeinfo['stop'](tun_path, tun_name)
335         raise
336 except:
337     modeinfo['dealloc'](tun_path, tun_name)
338     raise
339
340
341 # Trak SIGTERM, and set global termination flag instead of dying
342 TERMINATE = []
343 def _finalize(sig,frame):
344     global TERMINATE
345     TERMINATE.append(None)
346 signal.signal(signal.SIGTERM, _finalize)
347
348 try:
349     tcpdump = None
350     
351     if options.pass_fd:
352         if options.pass_fd.startswith("base64:"):
353             options.pass_fd = base64.b64decode(
354                 options.pass_fd[len("base64:"):])
355             options.pass_fd = os.path.expandvars(options.pass_fd)
356         
357         print >>sys.stderr, "Sending FD to: %r" % (options.pass_fd,)
358         
359         # send FD to whoever wants it
360         import passfd
361         
362         sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
363         retrydelay = 1.0
364         for i in xrange(30):
365             try:
366                 sock.connect(options.pass_fd)
367                 break
368             except socket.error:
369                 # wait a while, retry
370                 print >>sys.stderr, "%s: Could not connect. Retrying in a sec..." % (time.strftime('%c'),)
371                 time.sleep(min(30.0,retrydelay))
372                 retrydelay *= 1.1
373         else:
374             sock.connect(options.pass_fd)
375         passfd.sendfd(sock, tun.fileno(), '0')
376         
377         # Launch a tcpdump subprocess, to capture and dump packets,
378         # we will not be able to capture them ourselves.
379         # Make sure to catch sigterm and kill the tcpdump as well
380         tcpdump = subprocess.Popen(
381             ["tcpdump","-l","-n","-i",tun_name])
382         
383         # just wait forever
384         def tun_fwd(tun, remote):
385             while not TERMINATE:
386                 time.sleep(1)
387         remote = None
388     elif options.udp:
389         # connect to remote endpoint
390         if remaining_args and not remaining_args[0].startswith('-'):
391             print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.udp)
392             print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
393             rsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
394             retrydelay = 1.0
395             for i in xrange(30):
396                 try:
397                     rsock.bind((hostaddr,options.udp))
398                     break
399                 except socket.error:
400                     # wait a while, retry
401                     print >>sys.stderr, "%s: Could not bind. Retrying in a sec..." % (time.strftime('%c'),)
402                     time.sleep(min(30.0,retrydelay))
403                     retrydelay *= 1.1
404             else:
405                 rsock.bind((hostaddr,options.udp))
406             rsock.connect((remaining_args[0],options.port))
407         else:
408             print >>sys.stderr, "Error: need a remote endpoint in UDP mode"
409             raise AssertionError, "Error: need a remote endpoint in UDP mode"
410         remote = os.fdopen(rsock.fileno(), 'r+b', 0)
411     else:
412         # connect to remote endpoint
413         if remaining_args and not remaining_args[0].startswith('-'):
414             print >>sys.stderr, "Connecting to: %s:%d" % (remaining_args[0],options.port)
415             rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
416             retrydelay = 1.0
417             for i in xrange(30):
418                 try:
419                     rsock.connect((remaining_args[0],options.port))
420                     break
421                 except socket.error:
422                     # wait a while, retry
423                     print >>sys.stderr, "%s: Could not connect. Retrying in a sec..." % (time.strftime('%c'),)
424                     time.sleep(min(30.0,retrydelay))
425                     retrydelay *= 1.1
426             else:
427                 rsock.connect((remaining_args[0],options.port))
428         else:
429             print >>sys.stderr, "Listening at: %s:%d" % (hostaddr,options.port)
430             lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
431             retrydelay = 1.0
432             for i in xrange(30):
433                 try:
434                     lsock.bind((hostaddr,options.port))
435                     break
436                 except socket.error:
437                     # wait a while, retry
438                     print >>sys.stderr, "%s: Could not bind. Retrying in a sec..." % (time.strftime('%c'),)
439                     time.sleep(min(30.0,retrydelay))
440                     retrydelay *= 1.1
441             else:
442                 lsock.bind((hostaddr,options.port))
443             lsock.listen(1)
444             rsock,raddr = lsock.accept()
445         remote = os.fdopen(rsock.fileno(), 'r+b', 0)
446
447     print >>sys.stderr, "Connected"
448
449     tun_fwd(tun, remote)
450
451     if tcpdump:
452         os.kill(tcpdump.pid, signal.SIGTERM)
453         tcpdump.wait()
454 finally:
455     try:
456         print >>sys.stderr, "Shutting down..."
457     except:
458         # In case sys.stderr is broken
459         pass
460     
461     # tidy shutdown in every case - swallow exceptions
462     try:
463         modeinfo['stop'](tun_path, tun_name)
464     except:
465         pass
466
467     try:
468         modeinfo['tunclose'](tun_path, tun_name, tun)
469     except:
470         pass
471         
472     try:
473         modeinfo['dealloc'](tun_path, tun_name)
474     except:
475         pass
476     
477     print >>sys.stderr, "TERMINATED GRACEFULLY"
478