Monkey-patch NEMU to fix demo with recent iproute
[re6stnet.git] / demo / demo
1 #!/usr/bin/python
2 import math, nemu, os, signal, socket, subprocess, sys, time, weakref
3 from collections import defaultdict
4 IPTABLES = 'iptables'
5 SCREEN = 'screen'
6 VERBOSE = 4
7 REGISTRY='10.0.0.2'
8 CA_DAYS = 1000
9
10 #                       registry
11 #                           |.2
12 #                           |10.0.0
13 #                           |.1
14 #        ---------------Internet----------------
15 #        |.1                |.1                |.1
16 #        |10.1.0            |10.2.0            |
17 #        |.2                |.2                |
18 #    gateway1           gateway2           s3:10.0.1
19 #        |.1                |.1            |.2 |.3 |.4
20 #    s1:10.1.1          s2:10.2.1          m6  m7  m8
21 #    |.2     |.3        |.2 |.3 |.4        |
22 #    m1      m2         m3  m4  m5         m9
23 #
24
25 def disable_signal_on_children(sig):
26     pid = os.getpid()
27     sigint = signal.signal(sig, lambda *x: os.getpid() == pid and sigint(*x))
28 disable_signal_on_children(signal.SIGINT)
29
30 Node__add_interface = nemu.Node._add_interface
31 def _add_interface(node, iface):
32     iface.__dict__['node'] = weakref.proxy(node)
33     return Node__add_interface(node, iface)
34 nemu.Node._add_interface = _add_interface
35
36 def get_addr_data():
37     ipdata = backticks([IP_PATH, "-o", "addr", "list"])
38
39     byidx = {}
40     bynam = {}
41     for line in ipdata.split("\n"):
42         if line == "":
43             continue
44         match = re.search(r'^(\d+):\s+(\S+?)(:?)\s+(.*)', line)
45         if not match:
46             raise RuntimeError("Invalid `ip' command output")
47         idx = int(match.group(1))
48         name = match.group(2)
49         # <patch>
50         if name not in bynam:
51             bynam[name] = byidx[idx] = []
52             if match.group(3): # BBB: old iproute also shows link info
53                 continue
54         # </patch>
55         bynam[name].append(_parse_ip_addr(match.group(4)))
56     return byidx, bynam
57 nemu.iproute.get_addr_data.func_code = get_addr_data.func_code
58
59 # create nodes
60 for name in """internet=I registry=R
61                           gateway1=g1 machine1=1 machine2=2
62                           gateway2=g2 machine3=3 machine4=4 machine5=5
63                           machine6=6 machine7=7 machine8=8 machine9=9
64             """.split():
65     name, short = name.split('=')
66     globals()[name] = node = nemu.Node()
67     node.name = name
68     node.short = short
69     node._screen = node.Popen((SCREEN, '-DmS', name))
70     node.screen = (lambda name: lambda *cmd:
71         subprocess.call([SCREEN, '-r', name, '-X', 'eval'] + map(
72             "screen sh -c '%s; exec $SHELL'".__mod__, cmd)))(name)
73
74 # create switch
75 switch1 = nemu.Switch()
76 switch2 = nemu.Switch()
77 switch3 = nemu.Switch()
78
79 #create interfaces
80 re_if_0, in_if_0 = nemu.P2PInterface.create_pair(registry, internet)
81 in_if_1, g1_if_0 = nemu.P2PInterface.create_pair(internet, gateway1)
82 in_if_2, g2_if_0 = nemu.P2PInterface.create_pair(internet, gateway2)
83 m6_if_1, m9_if_0 = nemu.P2PInterface.create_pair(machine6, machine9)
84
85 g1_if_0_name = g1_if_0.name
86 gateway1.Popen((IPTABLES, '-t', 'nat', '-A', 'POSTROUTING', '-o', g1_if_0_name, '-j', 'MASQUERADE')).wait()
87 gateway1.Popen((IPTABLES, '-t', 'nat', '-N', 'MINIUPNPD')).wait()
88 gateway1.Popen((IPTABLES, '-t', 'nat', '-A', 'PREROUTING', '-i', g1_if_0_name, '-j', 'MINIUPNPD')).wait()
89 gateway1.Popen((IPTABLES, '-N', 'MINIUPNPD')).wait()
90 machine9.Popen(('sysctl', 'net.ipv6.conf.%s.accept_ra=2' % m9_if_0.name)).wait()
91
92 in_if_3 = nemu.NodeInterface(internet)
93 g1_if_1 = nemu.NodeInterface(gateway1)
94 g2_if_1 = nemu.NodeInterface(gateway2)
95 m1_if_0 = nemu.NodeInterface(machine1)
96 m2_if_0 = nemu.NodeInterface(machine2)
97 m3_if_0 = nemu.NodeInterface(machine3)
98 m4_if_0 = nemu.NodeInterface(machine4)
99 m5_if_0 = nemu.NodeInterface(machine5)
100 m6_if_0 = nemu.NodeInterface(machine6)
101 m7_if_0 = nemu.NodeInterface(machine7)
102 m8_if_0 = nemu.NodeInterface(machine8)
103
104 # connect to switch
105 switch1.connect(g1_if_1)
106 switch1.connect(m1_if_0)
107 switch1.connect(m2_if_0)
108
109 switch2.connect(g2_if_1)
110 switch2.connect(m3_if_0)
111 switch2.connect(m4_if_0)
112 switch2.connect(m5_if_0)
113
114 switch3.connect(in_if_3)
115 switch3.connect(m6_if_0)
116 switch3.connect(m7_if_0)
117 switch3.connect(m8_if_0)
118
119 # setting everything up
120 switch1.up = switch2.up = switch3.up = True
121 re_if_0.up = in_if_0.up = in_if_1.up = g1_if_0.up = in_if_2.up = g2_if_0.up = True
122 in_if_3.up = g1_if_1.up = g2_if_1.up = m1_if_0.up = m2_if_0.up = m3_if_0.up = m4_if_0.up = m5_if_0.up = m6_if_0.up = m6_if_1.up = m7_if_0.up = m8_if_0.up = m9_if_0.up = True
123
124 # Add IPv4 addresses
125 re_if_0.add_v4_address(address=REGISTRY, prefix_len=24)
126 in_if_0.add_v4_address(address='10.0.0.1', prefix_len=24)
127 in_if_1.add_v4_address(address='10.1.0.1', prefix_len=24)
128 in_if_2.add_v4_address(address='10.2.0.1', prefix_len=24)
129 in_if_3.add_v4_address(address='10.0.1.1', prefix_len=24)
130 in_if_3.add_v6_address(address='2001:db8::1', prefix_len=48)
131 g1_if_0.add_v4_address(address='10.1.0.2', prefix_len=24)
132 g1_if_1.add_v4_address(address='10.1.1.1', prefix_len=24)
133 g2_if_0.add_v4_address(address='10.2.0.2', prefix_len=24)
134 g2_if_1.add_v4_address(address='10.2.1.1', prefix_len=24)
135 m1_if_0.add_v4_address(address='10.1.1.2', prefix_len=24)
136 m2_if_0.add_v4_address(address='10.1.1.3', prefix_len=24)
137 m3_if_0.add_v4_address(address='10.2.1.2', prefix_len=24)
138 m4_if_0.add_v4_address(address='10.2.1.3', prefix_len=24)
139 m5_if_0.add_v4_address(address='10.2.1.4', prefix_len=24)
140 m6_if_0.add_v4_address(address='10.0.1.2', prefix_len=24)
141 m7_if_0.add_v4_address(address='10.0.1.3', prefix_len=24)
142 m8_if_0.add_v4_address(address='10.0.1.4', prefix_len=24)
143 m6_if_1.add_v4_address(address='192.168.241.1', prefix_len=24)
144
145 def add_llrtr(iface, peer, dst='default'):
146     for a in peer.get_addresses():
147         a = a['address']
148         if a.startswith('fe80:'):
149             return iface.node.Popen(('ip', 'route', 'add', dst, 'via', a,
150                 'proto', 'static', 'dev', iface.name)).wait()
151
152 # setup routes
153 add_llrtr(re_if_0, in_if_0)
154 add_llrtr(in_if_0, re_if_0, '2001:db8:42::/48')
155 registry.add_route(prefix='10.0.0.0', prefix_len=8, nexthop='10.0.0.1')
156 internet.add_route(prefix='10.2.0.0', prefix_len=16, nexthop='10.2.0.2')
157 gateway1.add_route(prefix='10.0.0.0', prefix_len=8, nexthop='10.1.0.1')
158 gateway2.add_route(prefix='10.0.0.0', prefix_len=8, nexthop='10.2.0.1')
159 for m in machine1, machine2:
160     m.add_route(nexthop='10.1.1.1')
161 for m in machine3, machine4, machine5:
162     m.add_route(prefix='10.0.0.0', prefix_len=8, nexthop='10.2.1.1')
163 for m in machine6, machine7, machine8:
164     m.add_route(prefix='10.0.0.0', prefix_len=8, nexthop='10.0.1.1')
165
166 # Test connectivity first. Run process, hide output and check
167 # return code
168 null = file(os.devnull, "r+")
169 for ip in '10.1.1.2', '10.1.1.3', '10.2.1.2', '10.2.1.3':
170     if machine1.Popen(('ping', '-c1', ip), stdout=null).wait():
171         print 'Failed to ping %s' % ip
172         break
173 else:
174     print "Connectivity IPv4 OK!"
175
176 nodes = []
177 gateway1.screen('miniupnpd -d -f miniupnpd.conf -P miniupnpd.pid -a 10.1.1.1'
178                 ' -i %s' % g1_if_0_name)
179 if 1:
180     import sqlite3
181     os.path.exists('ca.crt') or subprocess.check_call(
182         "openssl req -nodes -new -x509 -key registry/ca.key -out ca.crt"
183         " -subj /CN=re6st.example.com/emailAddress=re6st@example.com"
184         " -set_serial 0x120010db80042 -days %u" % CA_DAYS, shell=True)
185     db_path = 'registry/registry.db'
186     registry.screen('../re6st-registry @registry/re6st-registry.conf --db %s'
187         ' --mailhost %s -v%u' % (db_path, os.path.abspath('mbox'), VERBOSE))
188     registry_url = 'http://%s/' % REGISTRY
189     registry.Popen(('python', '-c', """if 1:
190         import socket, time
191         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
192         while True:
193             try:
194                 s.connect(('localhost', 80))
195                 break
196             except socket.error:
197                 time.sleep(.1)
198         """)).wait()
199     db = sqlite3.connect(db_path, isolation_level=None)
200     def re6stnet(node, folder, args='', prefix_len=None, registry=registry_url):
201         nodes.append(node)
202         if not os.path.exists(folder + '/cert.crt'):
203             dh_path = folder + '/dh2048.pem'
204             if not os.path.exists(dh_path):
205                 os.symlink('../dh2048.pem', dh_path)
206             email = node.name + '@example.com'
207             p = node.Popen(('../../re6st-conf', '--registry', registry,
208                 '--email', email), stdin=subprocess.PIPE, cwd=folder)
209             token = None
210             while not token:
211                 time.sleep(.1)
212                 token = db.execute("SELECT token FROM token WHERE email=?",
213                                    (email,)).fetchone()
214             if prefix_len:
215                 db.execute("UPDATE token SET prefix_len=? WHERE token=?",
216                            (prefix_len, token[0]))
217             p.communicate(str(token[0]))
218             os.remove(dh_path)
219             os.remove(folder + '/ca.crt')
220         node.screen('../re6stnet @%s/re6stnet.conf -v%u --registry %s %s'
221                     % (folder, VERBOSE, registry, args))
222     re6stnet(registry, 'registry', '--ip ' + REGISTRY, registry='http://localhost/')
223     re6stnet(machine1, 'm1', '-I%s' % m1_if_0.name)
224     re6stnet(machine2, 'm2', '--remote-gateway 10.1.1.1', prefix_len=80)
225     re6stnet(machine3, 'm3', '-i%s' % m3_if_0.name)
226     re6stnet(machine4, 'm4', '-i%s' % m4_if_0.name)
227     re6stnet(machine5, 'm5', '-i%s' % m5_if_0.name)
228     re6stnet(machine6, 'm6', '-I%s' % m6_if_1.name)
229     re6stnet(machine7, 'm7')
230     re6stnet(machine8, 'm8')
231     db.close()
232
233 _ll = {}
234 def node_by_ll(addr):
235     try:
236         return _ll[addr]
237     except KeyError:
238         for n in nodes:
239             for i in n.get_interfaces():
240                 t = isinstance(i, nemu.interface.ImportedNodeInterface)
241                 try:
242                     a = i.get_addresses()
243                 except KeyError:
244                     break
245                 for a in a:
246                     p = a['prefix_len']
247                     a = a['address']
248                     if a.startswith('2001:db8:'):
249                         assert not p % 8
250                         a = socket.inet_ntop(socket.AF_INET6,
251                             socket.inet_pton(socket.AF_INET6,
252                             a)[:p/8].ljust(16, '\0'))
253                     elif not a.startswith('fe80::'):
254                         continue
255                     _ll[a] = n, t
256     return _ll[addr]
257
258 def route_svg(z=4):
259     graph = {}
260     for n in nodes:
261         g = graph[n] = defaultdict(list)
262         for r in n.get_routes():
263             if r.prefix and r.prefix.startswith('2001:db8:'):
264                 try:
265                     g[node_by_ll(r.nexthop)].append(node_by_ll(r.prefix)[0])
266                 except KeyError:
267                     pass
268     gv = ["digraph { splines = true; edge[color=grey, labelangle=0, arrowhead=dot];"]
269     N = len(nodes)
270     a = 2 * math.pi / N
271     edges = set()
272     for i, n in enumerate(nodes):
273         gv.append('%s[pos="%s,%s!"];'
274             % (n.name, z * math.cos(a * i), z * math.sin(a * i)))
275         l = []
276         for p, r in graph[n].iteritems():
277             j = abs(nodes.index(p[0]) - i)
278             l.append((min(j, N - j), p, r))
279         for j, (l, (p, t), r) in enumerate(sorted(l)):
280             l = []
281             for r in sorted(r.short for r in r):
282                 if r == p.short:
283                     r = '<font color="grey">%s</font>' % r
284                 l.append(r)
285             if (n.name, p.name) in edges:
286                 r = 'penwidth=0'
287             else:
288                 edges.add((p.name, n.name))
289                 r = 'style=solid' if t else 'style=dashed'
290             gv.append('%s -> %s [labeldistance=%u, headlabel=<%s>, %s];'
291                 % (p.name, n.name, 1.5 * math.sqrt(j) + 2, ','.join(l), r))
292     gv.append('}\n')
293     return subprocess.Popen(('neato', '-Tsvg'),
294         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
295         ).communicate('\n'.join(gv))[0]
296
297 if len(sys.argv) > 1:
298     import SimpleHTTPServer, SocketServer
299
300     class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler):
301         def do_GET(self):
302             svg = None
303             if self.path == '/route.html':
304                 other = 'tunnel'
305                 svg = route_svg()
306             elif self.path == '/tunnel.html':
307                 other = 'route'
308                 gv = registry.Popen(('python', '-c', r"""if 1:
309                     import math
310                     from re6st.registry import RegistryClient
311                     g = eval(RegistryClient('http://localhost/').topology())
312                     print 'digraph {'
313                     a = 2 * math.pi / len(g)
314                     z = 4
315                     m2 = '%u/80' % (2 << 64)
316                     title = lambda n: '2|80' if n == m2 else n
317                     g = sorted((title(k), v) for k, v in g.iteritems())
318                     for i, (n, p) in enumerate(g):
319                         print '"%s"[pos="%s,%s!"%s];' % (title(n),
320                             z * math.cos(a * i), z * math.sin(a * i),
321                             ', style=dashed' if p is None else '')
322                         for p in p or ():
323                             print '"%s" -> "%s";' % (n, title(p))
324                     print '}'
325                 """), stdout=subprocess.PIPE, cwd="..").communicate()[0]
326                 if gv:
327                     svg = subprocess.Popen(('neato', '-Tsvg'),
328                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
329                         ).communicate(gv)[0]
330                 if not svg:
331                     self.send_error(500)
332                     return
333             else:
334                 if self.path == '/':
335                     self.send_response(302)
336                     self.send_header('Location', 'route.html')
337                     self.end_headers()
338                 else:
339                     self.send_error(404)
340                 return
341             mt = 'text/html'
342             body = """<html>
343 <head><meta http-equiv="refresh" content="10"/></head>
344 <body><a style="position: absolute" href="%s.html">%ss</a>
345 %s
346 </body>
347 </html>""" % (other, other, svg[svg.find('<svg'):])
348             self.send_response(200)
349             self.send_header('Content-Length', len(body))
350             self.send_header('Content-type', mt + '; charset=utf-8')
351             self.end_headers()
352             self.wfile.write(body)
353
354     class TCPServer(SocketServer.TCPServer):
355         allow_reuse_address = True
356
357     TCPServer(('', int(sys.argv[1])), Handler).serve_forever()
358
359 import pdb; pdb.set_trace()