TUN/TAP filters, initial version, with tests.
[nepi.git] / src / nepi / testbeds / planetlab / interfaces.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID
5 import nepi.util.ipaddr2 as ipaddr2
6 import nepi.util.server as server
7 import plcapi
8 import subprocess
9 import os
10 import os.path
11 import random
12 import ipaddr
13 import functools
14
15 import tunproto
16
17 class NodeIface(object):
18     def __init__(self, api=None):
19         if not api:
20             api = plcapi.PLCAPI()
21         self._api = api
22         
23         # Attributes
24         self.primary = True
25
26         # These get initialized at configuration time
27         self.address = None
28         self.lladdr = None
29         self.netprefix = None
30         self.netmask = None
31         self.broadcast = True
32         self._interface_id = None
33
34         # These get initialized when the iface is connected to its node
35         self.node = None
36
37         # These get initialized when the iface is connected to the internet
38         self.has_internet = False
39
40     def __str__(self):
41         return "%s<ip:%s/%s up mac:%s>" % (
42             self.__class__.__name__,
43             self.address, self.netmask,
44             self.lladdr,
45         )
46     
47     __repr__ = __str__
48
49     def add_address(self, address, netprefix, broadcast):
50         raise RuntimeError, "Cannot add explicit addresses to public interface"
51     
52     def pick_iface(self, siblings):
53         """
54         Picks an interface using the PLCAPI to query information about the node.
55         
56         Needs an assigned node.
57         
58         Params:
59             siblings: other NodeIface elements attached to the same node
60         """
61         
62         if self.node is None or self.node._node_id is None:
63             raise RuntimeError, "Cannot pick interface without an assigned node"
64         
65         avail = self._api.GetInterfaces(
66             node_id=self.node._node_id, 
67             is_primary=self.primary,
68             fields=('interface_id','mac','netmask','ip') )
69         
70         used = set([sibling._interface_id for sibling in siblings
71                     if sibling._interface_id is not None])
72         
73         for candidate in avail:
74             candidate_id = candidate['interface_id']
75             if candidate_id not in used:
76                 # pick it!
77                 self._interface_id = candidate_id
78                 self.address = candidate['ip']
79                 self.lladdr = candidate['mac']
80                 self.netprefix = candidate['netmask']
81                 self.netmask = ipaddr2.ipv4_dot2mask(self.netprefix) if self.netprefix else None
82                 return
83         else:
84             raise RuntimeError, "Cannot configure interface: cannot find suitable interface in PlanetLab node"
85
86     def validate(self):
87         if not self.has_internet:
88             raise RuntimeError, "All external interface devices must be connected to the Internet"
89     
90
91 class _CrossIface(object):
92     def __init__(self, proto, addr, port, cipher):
93         self.tun_proto = proto
94         self.tun_addr = addr
95         self.tun_port = port
96         self.tun_cipher = cipher
97         
98         # Cannot access cross peers
99         self.peer_proto_impl = None
100     
101     def __str__(self):
102         return "%s%r" % (
103             self.__class__.__name__,
104             ( self.tun_proto,
105               self.tun_addr,
106               self.tun_port,
107               self.tun_cipher ) 
108         )
109     
110     __repr__ = __str__
111
112 class TunIface(object):
113     _PROTO_MAP = tunproto.TUN_PROTO_MAP
114     _KIND = 'TUN'
115
116     def __init__(self, api=None):
117         if not api:
118             api = plcapi.PLCAPI()
119         self._api = api
120         
121         # Attributes
122         self.address = None
123         self.netprefix = None
124         self.netmask = None
125         
126         self.up = None
127         self.device_name = None
128         self.mtu = None
129         self.snat = False
130         self.txqueuelen = None
131         self.pointopoint = None
132         
133         # Enabled traces
134         self.capture = False
135
136         # These get initialized when the iface is connected to its node
137         self.node = None
138         
139         # These get initialized when the iface is connected to any filter
140         self.filter_module = None
141         
142         # These get initialized when the iface is configured
143         self.external_iface = None
144         
145         # These get initialized when the iface is configured
146         # They're part of the TUN standard attribute set
147         self.tun_port = None
148         self.tun_addr = None
149         self.tun_cipher = "AES"
150         
151         # These get initialized when the iface is connected to its peer
152         self.peer_iface = None
153         self.peer_proto = None
154         self.peer_addr = None
155         self.peer_port = None
156         self.peer_proto_impl = None
157         self._delay_recover = False
158
159         # same as peer proto, but for execute-time standard attribute lookups
160         self.tun_proto = None 
161         
162         
163         # Generate an initial random cryptographic key to use for tunnelling
164         # Upon connection, both endpoints will agree on a common one based on
165         # this one.
166         self.tun_key = ( ''.join(map(chr, [ 
167                     r.getrandbits(8) 
168                     for i in xrange(32) 
169                     for r in (random.SystemRandom(),) ])
170                 ).encode("base64").strip() )        
171         
172
173     def __str__(self):
174         return "%s<ip:%s/%s %s%s%s>" % (
175             self.__class__.__name__,
176             self.address, self.netprefix,
177             " up" if self.up else " down",
178             " snat" if self.snat else "",
179             (" p2p %s" % (self.pointopoint,)) if self.pointopoint else "",
180         )
181     
182     __repr__ = __str__
183     
184     @property
185     def if_name(self):
186         if self.peer_proto_impl:
187             return self.peer_proto_impl.if_name
188
189     def routes_here(self, route):
190         """
191         Returns True if the route should be attached to this interface
192         (ie, it references a gateway in this interface's network segment)
193         """
194         if self.address and self.netprefix:
195             addr, prefix = self.address, self.netprefix
196             pointopoint = self.pointopoint
197             if not pointopoint:
198                 pointopoint = self.peer_iface.address
199             
200             if pointopoint:
201                 prefix = 32
202                 
203             dest, destprefix, nexthop, metric = route
204             
205             myNet = ipaddr.IPNetwork("%s/%d" % (addr, prefix))
206             gwIp = ipaddr.IPNetwork(nexthop)
207             
208             if pointopoint:
209                 peerIp = ipaddr.IPNetwork(pointopoint)
210                 
211                 if gwIp == peerIp:
212                     return True
213             else:
214                 if gwIp in myNet:
215                     return True
216         return False
217     
218     def add_address(self, address, netprefix, broadcast):
219         if (self.address or self.netprefix or self.netmask) is not None:
220             raise RuntimeError, "Cannot add more than one address to %s interfaces" % (self._KIND,)
221         if broadcast:
222             raise ValueError, "%s interfaces cannot broadcast in PlanetLab (%s)" % (self._KIND,broadcast)
223         
224         self.address = address
225         self.netprefix = netprefix
226         self.netmask = ipaddr2.ipv4_mask2dot(netprefix)
227     
228     def validate(self):
229         if not self.node:
230             raise RuntimeError, "Unconnected %s iface - missing node" % (self._KIND,)
231         if self.peer_iface and self.peer_proto not in self._PROTO_MAP:
232             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
233         if not self.address or not self.netprefix or not self.netmask:
234             raise RuntimeError, "Misconfigured %s iface - missing address" % (self._KIND,)
235         if self.filter_module and self.peer_proto not in ('udp','tcp',None):
236             raise RuntimeError, "Miscofnigured TUN: %s - filtered tunnels only work with udp or tcp links" % (self,)
237         if self.tun_cipher != 'PLAIN' and self.peer_proto not in ('udp','tcp',None):
238             raise RuntimeError, "Miscofnigured TUN: %s - ciphered tunnels only work with udp or tcp links" % (self,)
239     
240     def _impl_instance(self, home_path, listening):
241         impl = self._PROTO_MAP[self.peer_proto](
242             self, self.peer_iface, home_path, self.tun_key, listening)
243         impl.port = self.tun_port
244         return impl
245     
246     def recover(self):
247         if self.peer_proto:
248             self.peer_proto_impl = self._impl_instance(
249                 self._home_path,
250                 False) # no way to know, no need to know
251             self.peer_proto_impl.recover()
252         else:
253             self._delay_recover = True
254     
255     def prepare(self, home_path, listening):
256         if not self.peer_iface and (self.peer_proto and (listening or (self.peer_addr and self.peer_port))):
257             # Ad-hoc peer_iface
258             self.peer_iface = _CrossIface(
259                 self.peer_proto,
260                 self.peer_addr,
261                 self.peer_port,
262                 self.peer_cipher)
263         if self.peer_iface:
264             if not self.peer_proto_impl:
265                 self.peer_proto_impl = self._impl_instance(home_path, listening)
266             if self._delay_recover:
267                 self.peer_proto_impl.recover()
268             else:
269                 self.peer_proto_impl.prepare()
270     
271     def setup(self):
272         if self.peer_proto_impl:
273             self.peer_proto_impl.setup()
274     
275     def cleanup(self):
276         if self.peer_proto_impl:
277             self.peer_proto_impl.shutdown()
278
279     def destroy(self):
280         if self.peer_proto_impl:
281             self.peer_proto_impl.destroy()
282             self.peer_proto_impl = None
283
284     def async_launch_wait(self):
285         if self.peer_proto_impl:
286             self.peer_proto_impl.async_launch_wait()
287
288     def sync_trace(self, local_dir, whichtrace):
289         if self.peer_proto_impl:
290             return self.peer_proto_impl.sync_trace(local_dir, whichtrace)
291         else:
292             return None
293
294     def remote_trace_path(self, whichtrace):
295         if self.peer_proto_impl:
296             return self.peer_proto_impl.remote_trace_path(whichtrace)
297         else:
298             return None
299
300 class TapIface(TunIface):
301     _PROTO_MAP = tunproto.TAP_PROTO_MAP
302     _KIND = 'TAP'
303
304 # Yep, it does nothing - yet
305 class Internet(object):
306     def __init__(self, api=None):
307         if not api:
308             api = plcapi.PLCAPI()
309         self._api = api
310
311 class NetPipe(object):
312     def __init__(self, api=None):
313         if not api:
314             api = plcapi.PLCAPI()
315         self._api = api
316
317         # Attributes
318         self.mode = None
319         self.addrList = None
320         self.portList = None
321         
322         self.plrIn = None
323         self.bwIn = None
324         self.delayIn = None
325
326         self.plrOut = None
327         self.bwOut = None
328         self.delayOut = None
329         
330         # These get initialized when the pipe is connected to its node
331         self.node = None
332         self.configured = False
333     
334     def validate(self):
335         if not self.mode:
336             raise RuntimeError, "Undefined NetPipe mode"
337         if not self.portList:
338             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
339         if not (self.plrIn or self.bwIn or self.delayIn):
340             raise RuntimeError, "Undefined NetPipe inbound characteristics"
341         if not (self.plrOut or self.bwOut or self.delayOut):
342             raise RuntimeError, "Undefined NetPipe outbound characteristics"
343         if not self.node:
344             raise RuntimeError, "Unconnected NetPipe"
345     
346     def _add_pipedef(self, bw, plr, delay, options):
347         if delay:
348             options.extend(("delay","%dms" % (delay,)))
349         if bw:
350             options.extend(("bw","%.8fMbit/s" % (bw,)))
351         if plr:
352             options.extend(("plr","%.8f" % (plr,)))
353     
354     def _get_ruledef(self):
355         scope = "%s%s%s" % (
356             self.portList,
357             "@" if self.addrList else "",
358             self.addrList or "",
359         )
360         
361         options = []
362         if self.bwIn or self.plrIn or self.delayIn:
363             options.append("IN")
364             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
365         if self.bwOut or self.plrOut or self.delayOut:
366             options.append("OUT")
367             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
368         options = ' '.join(options)
369         
370         return (scope,options)
371     
372     def recover(self):
373         # Rules are safe on their nodes
374         self.configured = True
375
376     def configure(self):
377         # set up rule
378         scope, options = self._get_ruledef()
379         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
380         
381         (out,err),proc = server.popen_ssh_command(
382             command,
383             host = self.node.hostname,
384             port = None,
385             user = self.node.slicename,
386             agent = None,
387             ident_key = self.node.ident_path,
388             server_key = self.node.server_key
389             )
390     
391         if proc.wait():
392             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
393         
394         # we have to clean up afterwards
395         self.configured = True
396     
397     def refresh(self):
398         if self.configured:
399             # refresh rule
400             scope, options = self._get_ruledef()
401             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
402             
403             (out,err),proc = server.popen_ssh_command(
404                 command,
405                 host = self.node.hostname,
406                 port = None,
407                 user = self.node.slicename,
408                 agent = None,
409                 ident_key = self.node.ident_path,
410                 server_key = self.node.server_key
411                 )
412         
413             if proc.wait():
414                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
415     
416     def cleanup(self):
417         if self.configured:
418             # remove rule
419             scope, options = self._get_ruledef()
420             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
421             
422             (out,err),proc = server.popen_ssh_command(
423                 command,
424                 host = self.node.hostname,
425                 port = None,
426                 user = self.node.slicename,
427                 agent = None,
428                 ident_key = self.node.ident_path,
429                 server_key = self.node.server_key
430                 )
431         
432             if proc.wait():
433                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
434             
435             self.configured = False
436     
437     def sync_trace(self, local_dir, whichtrace):
438         if whichtrace != 'netpipeStats':
439             raise ValueError, "Unsupported trace %s" % (whichtrace,)
440         
441         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
442         
443         # create parent local folders
444         proc = subprocess.Popen(
445             ["mkdir", "-p", os.path.dirname(local_path)],
446             stdout = open("/dev/null","w"),
447             stdin = open("/dev/null","r"))
448
449         if proc.wait():
450             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
451         
452         (out,err),proc = server.popen_ssh_command(
453             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
454             host = self.node.hostname,
455             port = None,
456             user = self.node.slicename,
457             agent = None,
458             ident_key = self.node.ident_path,
459             server_key = self.node.server_key
460             )
461         
462         if proc.wait():
463             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
464         
465         # dump results to file
466         f = open(local_path, "wb")
467         f.write(err or "")
468         f.write(out or "")
469         f.close()
470         
471         return local_path
472     
473 class TunFilter(object):
474     def __init__(self, api=None):
475         if not api:
476             api = plcapi.PLCAPI()
477         self._api = api
478         
479         # Attributes
480         self.module = None
481
482         # These get initialised when the filter is connected
483         self.peer_guid = None
484         self.peer_proto = None
485         self.iface_guid = None
486         self.peer = None
487         self.iface = None
488     
489     def _get(what, self):
490         wref = self.iface
491         if wref:
492             wref = wref()
493         if wref:
494             return getattr(wref, what)
495         else:
496             return None
497
498     def _set(what, self, val):
499         wref = self.iface
500         if wref:
501             wref = wref()
502         if wref:
503             setattr(wref, what, val)
504     
505     tun_proto = property(
506         functools.partial(_get, 'tun_proto'),
507         functools.partial(_set, 'tun_proto') )
508     tun_addr = property(
509         functools.partial(_get, 'tun_addr'),
510         functools.partial(_set, 'tun_addr') )
511     tun_port = property(
512         functools.partial(_get, 'tun_port'),
513         functools.partial(_set, 'tun_port') )
514     tun_key = property(
515         functools.partial(_get, 'tun_key'),
516         functools.partial(_set, 'tun_key') )
517     tun_cipher = property(
518         functools.partial(_get, 'tun_cipher'),
519         functools.partial(_set, 'tun_cipher') )
520     
521     del _get
522     del _set
523