Add support for recent iproute, which now recognizes babel protocol
[re6stnet.git] / re6st / cli / node.py
1 #!/usr/bin/python
2 import atexit, errno, logging, os, shutil, signal
3 import socket, struct, subprocess, sys, time, threading
4 from collections import deque
5 from functools import partial
6 if 're6st' not in sys.modules:
7 sys.path[0] = os.path.dirname(os.path.dirname(sys.path[0]))
8 from re6st import plib, tunnel, utils, version, x509
9 from re6st.cache import Cache
10 from re6st.utils import exit, ReexecException
11
12 def getConfig():
13 parser = utils.ArgParser(fromfile_prefix_chars='@',
14 description="Resilient virtual private network application.")
15 _ = parser.add_argument
16 _('-V', '--version', action='version', version=version.version)
17
18 _('--ip', action='append', default=[],
19 help="IP address advertised to other nodes. Special values:\n"
20 "- upnp: redirect ports when UPnP device is found\n"
21 "- any: ask peers our IP\n"
22 " (default: like 'upnp' if miniupnpc is installed,\n"
23 " otherwise like 'any')")
24 _('--registry', metavar='URL', required=True,
25 help="Public HTTP URL of the registry, for bootstrapping.")
26 _('-l', '--log', default='/var/log/re6stnet',
27 help="Path to the directory used for log files:\n"
28 "- re6stnet.log: log file of re6stnet itself\n"
29 "- babeld.log: log file of router\n"
30 "- <iface>.log: 1 file per spawned OpenVPN\n")
31 _('-r', '--run', default='/var/run/re6stnet',
32 help="Path to re6stnet runtime directory:\n"
33 "- babeld.pid (option -I of babeld)\n"
34 "- babeld.sock (option -R of babeld)\n")
35 _('-s', '--state', default='/var/lib/re6stnet',
36 help="Path to re6stnet state directory:\n"
37 "- cache.db: cache of network parameters and peer addresses\n"
38 "- babeld.state: see option -S of babeld\n")
39 _('-v', '--verbose', default=1, type=int, metavar='LEVEL',
40 help="Log level of re6stnet itself. 0 disables logging. 1=WARNING,"
41 " 2=INFO, 3=DEBUG, 4=TRACE. Use SIGUSR1 to reopen log."
42 " See also --babel-verb and --verb for logs of spawned processes.")
43 _('-i', '--interface', action='append', dest='iface_list', default=[],
44 help="Extra interface for LAN discovery. Highly recommanded if there"
45 " are other re6st node on the same network segment.")
46 _('-I', '--main-interface', metavar='IFACE', default='lo',
47 help="Set re6stnet IP on given interface. Any interface not used for"
48 " tunnelling can be chosen.")
49 _('--up', metavar='CMD',
50 help="Shell command to run after successful initialization.")
51 _('--daemon', action='append', metavar='CMD',
52 help="Same as --up, but run in background: the command will be killed"
53 " at exit (with a TERM signal, followed by KILL 5 seconds later"
54 " if process is still alive).")
55 _('--test', metavar='EXPR',
56 help="Exit after configuration parsing. Status code is the"
57 " result of the given Python expression. For example:\n"
58 " main_interface != 'eth0'")
59
60 _ = parser.add_argument_group('routing').add_argument
61 _('-B', dest='babel_args', metavar='ARG', action='append', default=[],
62 help="Extra arguments to forward to Babel.")
63 _('-D', '--default', action='store_true',
64 help="Access internet via this network (in this case, make sure you"
65 " don't already have a default route), or if your kernel was"
66 " compiled without support for source address based routing"
67 " (CONFIG_IPV6_SUBTREES). Meaningless with --gateway.")
68 _('--table', type=int, choices=(0,),
69 help="DEPRECATED: Use --default instead of --table=0")
70 _('--gateway', action='store_true',
71 help="Act as a gateway for this network (the default route will be"
72 " exported). Do never use it if you don't know what it means.")
73
74 _ = parser.add_argument_group('tunnelling').add_argument
75 _('-O', dest='openvpn_args', metavar='ARG', action='append', default=[],
76 help="Extra arguments to forward to both server and client OpenVPN"
77 " subprocesses. Often used to configure verbosity.")
78 _('--ovpnlog', action='store_true',
79 help="Tell each OpenVPN subprocess to log to a dedicated file.")
80 _('--pp', nargs=2, action='append', metavar=('PORT', 'PROTO'),
81 help="Port and protocol to be announced to other peers, ordered by"
82 " preference. For each protocol (udp, tcp, udp6, tcp6), start one"
83 " openvpn server on the first given port."
84 " (default: --pp 1194 udp --pp 1194 tcp)")
85 _('--dh',
86 help="File containing Diffie-Hellman parameters in .pem format"
87 " (default: DH from registry)")
88 _('--ca', required=True, help=parser._ca_help)
89 _('--cert', required=True,
90 help="Local peer's signed certificate in .pem format."
91 " Common name defines the allocated prefix in the network.")
92 _('--key', required=True,
93 help="Local peer's private key in .pem format.")
94 _('--client-count', type=int,
95 help="Number of client tunnels to set up."
96 " (default: value from registry)")
97 _('--max-clients', type=int,
98 help="Maximum number of accepted clients per OpenVPN server."
99 " (default: value from registry)")
100 _('--remote-gateway', action='append', dest='gw_list',
101 help="Force each tunnel to be created through one the given gateways,"
102 " in a round-robin fashion.")
103 _('--disable-proto', action='append',
104 choices=('none', 'udp', 'tcp', 'udp6', 'tcp6'), default=['udp', 'udp6'],
105 help="Do never try to create tunnels using given protocols."
106 " 'none' has precedence over other options.")
107 _('--client', metavar='HOST,PORT,PROTO[;...]',
108 help="Do not run any OpenVPN server, but only 1 OpenVPN client,"
109 " with specified remotes. Any other option not required in this"
110 " mode is ignored (e.g. client-count, max-clients, etc.)")
111 _('--neighbour', metavar='CN', action='append', default=[],
112 help="List of peers that should be reachable directly, by creating"
113 " tunnels if necesssary.")
114
115 return parser.parse_args()
116
117 def main():
118 # Get arguments
119 config = getConfig()
120 cert = x509.Cert(config.ca, config.key, config.cert)
121 config.openvpn_args += cert.openvpn_args
122
123 if config.test:
124 sys.exit(eval(config.test, None, config.__dict__))
125
126 # Set logging
127 utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
128
129 logging.trace("Environment: %r", os.environ)
130 logging.trace("Configuration: %r", config)
131 utils.makedirs(config.state)
132 db_path = os.path.join(config.state, 'cache.db')
133 if config.ovpnlog:
134 plib.ovpn_log = config.log
135
136 exit.signal(0, signal.SIGINT, signal.SIGTERM)
137 exit.signal(-1, signal.SIGHUP, signal.SIGUSR2)
138
139 cache = Cache(db_path, config.registry, cert)
140 network = cert.network
141
142 if config.client_count is None:
143 config.client_count = cache.client_count
144 if config.max_clients is None:
145 config.max_clients = cache.max_clients
146
147 if config.table is not None:
148 logging.warning("--table option is deprecated: use --default instead")
149 config.default = True
150 if config.default and config.gateway:
151 sys.exit("error: conflicting options --default and --gateway")
152
153 if 'none' in config.disable_proto:
154 config.disable_proto = ()
155 if config.default:
156 # Make sure we won't tunnel over re6st.
157 config.disable_proto = tuple(set(('tcp6', 'udp6')).union(
158 config.disable_proto))
159 address = ()
160 server_tunnels = {}
161 forwarder = None
162 if config.client:
163 config.babel_args.append('re6stnet')
164 elif config.max_clients:
165 if config.pp:
166 pp = [(int(port), proto) for port, proto in config.pp]
167 for port, proto in pp:
168 if proto in config.disable_proto:
169 sys.exit("error: conflicting options --disable-proto %s"
170 " and --pp %u %s" % (proto, port, proto))
171 else:
172 pp = [x for x in ((1194, 'udp'), (1194, 'tcp'))
173 if x[1] not in config.disable_proto]
174 def ip_changed(ip):
175 for family, proto_list in ((socket.AF_INET, ('tcp', 'udp')),
176 (socket.AF_INET6, ('tcp6', 'udp6'))):
177 try:
178 socket.inet_pton(family, ip)
179 break
180 except socket.error:
181 pass
182 else:
183 family = None
184 return family, [(ip, str(port), proto) for port, proto in pp
185 if not family or proto in proto_list]
186 if config.gw_list:
187 gw_list = deque(config.gw_list)
188 def remote_gateway(dest):
189 gw_list.rotate()
190 return gw_list[0]
191 else:
192 remote_gateway = None
193 if len(config.ip) > 1:
194 if 'upnp' in config.ip or 'any' in config.ip:
195 sys.exit("error: argument --ip can be given only once with"
196 " 'any' or 'upnp' value")
197 logging.info("Multiple --ip passed: note that re6st does nothing to"
198 " make sure that incoming paquets are replied via the correct"
199 " gateway. So without manual network configuration, this can"
200 " not be used to accept server connections from multiple"
201 " gateways.")
202 if 'upnp' in config.ip or not config.ip:
203 logging.info('Attempting automatic configuration via UPnP...')
204 try:
205 from re6st.upnpigd import Forwarder
206 forwarder = Forwarder('re6stnet openvpn server')
207 except Exception, e:
208 if config.ip:
209 raise
210 logging.info("%s: assume we are not NATed", e)
211 else:
212 atexit.register(forwarder.clear)
213 for port, proto in pp:
214 forwarder.addRule(port, proto)
215 ip_changed = forwarder.checkExternalIp
216 address = ip_changed(),
217 elif 'any' not in config.ip:
218 address = map(ip_changed, config.ip)
219 ip_changed = None
220 for x in pp:
221 server_tunnels.setdefault('re6stnet-' + x[1], x)
222 else:
223 ip_changed = remote_gateway = None
224
225 def call(cmd):
226 logging.debug('%r', cmd)
227 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
228 stderr=subprocess.PIPE)
229 stdout, stderr = p.communicate()
230 if p.returncode:
231 raise EnvironmentError("%r failed with error %u\n%s"
232 % (' '.join(cmd), p.returncode, stderr))
233 return stdout
234 def ip4(object, *args):
235 args = ['ip', '-4', object, 'add'] + list(args)
236 call(args)
237 args[3] = 'del'
238 cleanup.append(lambda: subprocess.call(args))
239 def ip(object, *args):
240 args = ['ip', '-6', object, 'add'] + list(args)
241 call(args)
242 args[3] = 'del'
243 cleanup.append(lambda: subprocess.call(args))
244
245 try:
246 subnet = network + cert.prefix
247 my_ip = utils.ipFromBin(subnet, '1')
248 my_subnet = '%s/%u' % (utils.ipFromBin(subnet), len(subnet))
249 my_network = "%s/%u" % (utils.ipFromBin(network), len(network))
250 os.environ['re6stnet_ip'] = my_ip
251 os.environ['re6stnet_iface'] = config.main_interface
252 os.environ['re6stnet_subnet'] = my_subnet
253 os.environ['re6stnet_network'] = my_network
254
255 # Init db and tunnels
256 config.babel_args += server_tunnels
257 timeout = 4 * cache.hello
258 cleanup = [lambda: cache.cacheMinimize(config.client_count),
259 lambda: shutil.rmtree(config.run, True)]
260 utils.makedirs(config.run, 0700)
261 control_socket = os.path.join(config.run, 'babeld.sock')
262 if config.client_count and not config.client:
263 tunnel_manager = tunnel.TunnelManager(control_socket,
264 cache, cert, config.openvpn_args, timeout,
265 config.client_count, config.iface_list, address, ip_changed,
266 remote_gateway, config.disable_proto, config.neighbour)
267 config.babel_args += tunnel_manager.new_iface_list
268 else:
269 tunnel_manager = tunnel.BaseTunnelManager(cache, cert)
270 cleanup.append(tunnel_manager.sock.close)
271
272 try:
273 exit.acquire()
274
275 ipv4 = getattr(cache, 'ipv4', None)
276 if ipv4:
277 serial = int(cert.cert.get_subject().serialNumber)
278 if cache.ipv4_sublen <= 16 and serial < 1 << cache.ipv4_sublen:
279 dot4 = lambda x: socket.inet_ntoa(struct.pack('!I', x))
280 ip4('route', 'unreachable', ipv4, 'proto', 'static')
281 ipv4, n = ipv4.split('/')
282 ipv4, = struct.unpack('!I', socket.inet_aton(ipv4))
283 n = int(n) + cache.ipv4_sublen
284 x = ipv4 | serial << 32 - n
285 ipv4 = dot4(x | (n < 31))
286 config.openvpn_args += '--ifconfig', \
287 ipv4, dot4((1<<32) - (1<<32-n))
288 ipv4 = ipv4, n
289 if not isinstance(tunnel_manager, tunnel.TunnelManager):
290 ip4('addr', "%s/%s" % ipv4,
291 'dev', config.main_interface)
292 if config.main_interface == "lo":
293 ip4('route', 'unreachable', "%s/%s" % (dot4(x), n),
294 'proto', 'static')
295 else:
296 logging.warning(
297 "IPv4 payload disabled due to wrong network parameters")
298 ipv4 = None
299
300 if os.uname()[2] < '2.6.40': # BBB
301 logging.warning("Fallback to ip-addrlabel because Linux < 3.0"
302 " does not support RTA_PREFSRC for ipv6. Note however that"
303 " this workaround does not work with extra interfaces that"
304 " already have a public IP")
305 ip('addrlabel', 'prefix', my_network, 'label', '99')
306 # No need to tell babeld not to set a preferred source IP in
307 # installed routes. The kernel will silently discard the option.
308 R = {}
309 if config.client:
310 address_list = [x for x in utils.parse_address(config.client)
311 if x[2] not in config.disable_proto]
312 if not address_list:
313 sys.exit("error: --disable_proto option disables"
314 " all addresses given by --client")
315 cleanup.append(plib.client('re6stnet',
316 address_list, cache.encrypt, '--ping-restart',
317 str(timeout), *config.openvpn_args).stop)
318 elif server_tunnels:
319 dh = config.dh
320 if not dh:
321 dh = os.path.join(config.state, "dh.pem")
322 cache.getDh(dh)
323 for iface, (port, proto) in server_tunnels.iteritems():
324 r, x = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
325 cleanup.append(plib.server(iface, config.max_clients,
326 dh, x.fileno(), port, proto, cache.encrypt,
327 '--ping-exit', str(timeout), *config.openvpn_args,
328 preexec_fn=r.close).stop)
329 R[r] = partial(tunnel_manager.handleServerEvent, r)
330 x.close()
331
332 ip('addr', my_ip + '/%s' % len(subnet),
333 'dev', config.main_interface)
334 if_rt = ['ip', '-6', 'route', 'del',
335 'fe80::/64', 'dev', config.main_interface]
336 if config.main_interface == 'lo':
337 # WKRD: Removed this useless route now, since the kernel does
338 # not even remove it on exit.
339 subprocess.call(if_rt)
340 if_rt[4] = my_subnet
341 cleanup.append(lambda: subprocess.call(if_rt))
342 if config.default:
343 def check_no_default_route():
344 for route in call(('ip', '-6', 'route', 'show',
345 'default')).splitlines():
346 if not (' proto babel ' in route
347 or ' proto 42 ' in route):
348 sys.exit("Detected default route (%s)"
349 " whereas you specified --default."
350 " Fix your configuration." % route)
351 check_no_default_route()
352 def check_no_default_route_thread():
353 try:
354 while True:
355 time.sleep(60)
356 try:
357 check_no_default_route()
358 except OSError, e:
359 if e.errno != errno.ENOMEM:
360 raise
361 except:
362 utils.log_exception()
363 finally:
364 exit.kill_main(1)
365 t = threading.Thread(target=check_no_default_route_thread)
366 t.daemon = True
367 t.start()
368 ip('route', 'unreachable', my_network)
369
370 config.babel_args += config.iface_list
371 cleanup.append(plib.router((my_ip, len(subnet)), ipv4,
372 None if config.gateway else
373 '' if config.default else
374 my_network, cache.hello,
375 os.path.join(config.log, 'babeld.log'),
376 os.path.join(config.state, 'babeld.state'),
377 os.path.join(config.run, 'babeld.pid'),
378 control_socket, cache.babel_default,
379 *config.babel_args).stop)
380 if config.up:
381 exit.release()
382 r = os.system(config.up)
383 if r:
384 sys.exit(r)
385 exit.acquire()
386 for cmd in config.daemon or ():
387 cleanup.insert(-1, utils.Popen(cmd, shell=True).stop)
388 try:
389 cleanup[-1:-1] = (tunnel_manager.delInterfaces,
390 tunnel_manager.killAll)
391 except AttributeError:
392 pass
393
394 # main loop
395 exit.release()
396 select_list = [forwarder.select] if forwarder else []
397 select_list += tunnel_manager.select, utils.select
398 while True:
399 args = R.copy(), {}, []
400 for s in select_list:
401 s(*args)
402 finally:
403 # XXX: We have a possible race condition if a signal is handled at
404 # the beginning of this clause, just before the following line.
405 exit.acquire(0) # inhibit signals
406 while cleanup:
407 try:
408 cleanup.pop()()
409 except:
410 pass
411 exit.release()
412 except ReexecException, e:
413 logging.info(e)
414 except Exception:
415 utils.log_exception()
416 sys.exit(1)
417 try:
418 sys.exitfunc()
419 finally:
420 os.execvp(sys.argv[0], sys.argv)
421
422 if __name__ == "__main__":
423 main()