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