New --is-needed option to test whether re6st should be setup or not
[re6stnet.git] / re6stnet
1 #!/usr/bin/python
2 import atexit, errno, logging, os, random, select, signal
3 import sqlite3, subprocess, sys, time, traceback
4 from re6st import plib, utils, db, tunnel
5
6
7 def getConfig():
8     parser = utils.ArgParser(fromfile_prefix_chars='@',
9         description="Resilient virtual private network application.")
10     _ = parser.add_argument
11
12     _('--ip',
13         help="IP address advertised to other nodes. Special values:\n"
14              "- upnp: redirect ports when UPnP device is found\n"
15              "- any: ask peers our IP\n"
16              " (default: like 'upnp' if miniupnpc is installed,\n"
17              "  otherwise like 'any')")
18     _('--registry', metavar='URL',
19         help="Public HTTP URL of the registry, for bootstrapping.")
20     _('-l', '--log', default='/var/log/re6stnet',
21         help="Path to the directory used for log files:\n"
22              "- re6stnet.log: log file of re6stnet itself\n"
23              "- babeld.log: log file of router\n"
24              "- <iface>.log: 1 file per spawned OpenVPN\n")
25     _('-s', '--state', default='/var/lib/re6stnet',
26         help="Path to re6stnet state directory:\n"
27              "- peers.db: cache of peer addresses\n"
28              "- babeld.state: see option -S of babeld\n")
29     _('-v', '--verbose', default=1, type=int, metavar='LEVEL',
30         help="Log level of re6stnet itself. 0 disables logging."
31              " Use SIGUSR1 to reopen log."
32              " See also --babel-verb and --verb for logs of spawned processes.")
33     _('-i', '--interface', action='append', dest='iface_list', default=[],
34         help="Extra interface for LAN discovery. Highly recommanded if there"
35              " are other re6st node on the same network segment.")
36     _('-I', '--main-interface', metavar='IFACE', default='lo',
37         help="Set re6stnet IP on given interface. Any interface not used for"
38              " tunnelling can be chosen.")
39     _('--up', metavar='CMD',
40         help="Shell command to run after successful initialization.")
41     _('--daemon', action='append', metavar='CMD',
42         help="Same as --up, but run in background: the command will be killed"
43              " at exit (with a TERM signal, followed by KILL 5 seconds later"
44              " if process is still alive).")
45     _('--test', metavar='EXPR',
46         help="Exit after configuration parsing. Status code is the"
47              " result of the given Python expression. For example:\n"
48              "  main_interface != 'eth0'")
49
50     _ = parser.add_argument_group('routing').add_argument
51     _('-B', dest='babel_args', metavar='ARG', action='append', default=[],
52         help="Extra arguments to forward to Babel.")
53     _('--babel-pidfile', metavar='PID', default='/var/run/re6st-babeld.pid',
54         help="Specify a file to write our process id to"
55              " (option -I of Babel).")
56     _('--hello', type=int, default=15,
57         help="Hello interval in seconds, for both wired and wireless"
58              " connections. OpenVPN ping-exit option is set to 4 times the"
59              " hello interval. It takes between 3 and 4 times the"
60              " hello interval for Babel to re-establish connection with a"
61              " node for which the direct connection has been cut.")
62     _('--table', type=int, default=42,
63         help="Use given table id. Set 0 to use the main table, if:\n"
64              "- you are a gateway of this network (the default route will be"
65              "  exported)\n"
66              "- or you want to use the default route of this network for all"
67              " communications (in this case, make sure you don't already have"
68              " a default route).\n")
69     _ = parser.add_argument_group('tunnelling').add_argument
70     _('-O', dest='openvpn_args', metavar='ARG', action='append', default=[],
71         help="Extra arguments to forward to both server and client OpenVPN"
72              " subprocesses. Often used to configure verbosity.")
73     _('--ovpnlog', action='store_true',
74         help="Tell each OpenVPN subprocess to log to a dedicated file.")
75     _('--encrypt', action='store_true',
76         help='Specify that tunnels should be encrypted.')
77     _('--pp', nargs=2, action='append', metavar=('PORT', 'PROTO'),
78         help="Port and protocol to be announced to other peers, ordered by"
79              " preference. For each protocol (either udp or tcp), start one"
80              " openvpn server on the first given port."
81              " (default: --pp 1194 udp --pp 1194 tcp)")
82     _('--dh',
83         help='File containing Diffie-Hellman parameters in .pem format')
84     _('--ca', required=True, help=parser._ca_help)
85     _('--cert', required=True,
86         help="Local peer's signed certificate in .pem format."
87              " Common name defines the allocated prefix in the network.")
88     _('--key', required=True,
89         help="Local peer's private key in .pem format.")
90     _('--client-count', default=10, type=int,
91         help="Number of client tunnels to set up.")
92     _('--max-clients', type=int,
93         help="Maximum number of accepted clients per OpenVPN server. (default:"
94              " client-count * 2, which actually represents the average number"
95              " of tunnels to other peers)")
96     _('--tunnel-refresh', default=300, type=int,
97         help="Interval in seconds between two tunnel refresh: the worst"
98              " tunnel is closed if the number of client tunnels has reached"
99              " its maximum number (client-count).")
100     _('--remote-gateway', action='append', dest='gw_list',
101         help="Force each tunnel to be created through one the given gateways,"
102              " randomly.")
103     _('--client', metavar='HOST,PORT,PROTO[;...]',
104         help="Do not run any OpenVPN server, but only 1 OpenVPN client,"
105              " with specified remotes. Any other option not required in this"
106              " mode is ignored (e.g. client-count, max-clients, etc.)")
107
108     return parser.parse_args()
109
110
111 def main():
112     # Get arguments
113     config = getConfig()
114     with open(config.ca) as f:
115         network = utils.networkFromCa(f.read())
116     with open(config.cert) as f:
117         prefix = utils.binFromSubnet(utils.subnetFromCert(f.read()))
118     config.openvpn_args += (
119         '--ca', config.ca,
120         '--cert', config.cert,
121         '--key', config.key)
122     # TODO: verify certificates (should we moved to M2Crypto ?)
123
124     if config.test:
125         sys.exit(eval(config.test, None, config.__dict__))
126
127     # Set logging
128     utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
129
130     logging.trace("Environment: %r", os.environ)
131     logging.trace("Configuration: %r", config)
132     utils.makedirs(config.state)
133     db_path = os.path.join(config.state, 'peers.db')
134     if config.ovpnlog:
135         plib.ovpn_log = config.log
136
137     signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
138     signal.signal(signal.SIGTERM, lambda *args: sys.exit())
139
140     if config.max_clients is None:
141         config.max_clients = config.client_count * 2
142
143     address = []
144     server_tunnels = {}
145     if config.client:
146         config.babel_args.append('re6stnet')
147     elif config.max_clients:
148         if config.pp:
149             pp = [(int(port), proto) for port, proto in config.pp]
150         else:
151             pp = (1194, 'udp'), (1194, 'tcp')
152         ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
153         remote_gateway = config.gw_list and (lambda _:
154             random.choice(config.gw_list))
155         forwarder = None
156         if config.ip == 'upnp' or not config.ip:
157             logging.info('Attempting automatic configuration via UPnP...')
158             try:
159                 from re6st.upnpigd import Forwarder
160                 forwarder = Forwarder('re6stnet openvpn server')
161             except Exception, e:
162                 if config.ip:
163                     raise
164                 logging.info("%s: assume we are not NATed", e)
165             else:
166                 atexit.register(forwarder.clear)
167                 for port, proto in pp:
168                     forwarder.addRule(port, proto)
169                 ip_changed = forwarder.checkExternalIp
170                 address = ip_changed()
171         elif config.ip != 'any':
172             address = ip_changed(config.ip)
173             ip_changed = None
174         for x in pp:
175             server_tunnels.setdefault('re6stnet-' + x[1], x)
176
177     def call(cmd):
178         logging.debug('%r', cmd)
179         p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
180                                   stderr=subprocess.PIPE)
181         stdout, stderr = p.communicate()
182         if p.returncode:
183             raise EnvironmentError("%r failed with error %u\n%s"
184                                    % (' '.join(cmd), p.returncode, stderr))
185         return stdout
186     def required(arg):
187         if not getattr(config, arg):
188             sys.exit("error: argument --%s is required" % arg)
189     def ip(object, *args):
190         args = ['ip', '-6', object, 'add'] + list(args)
191         call(args)
192         args[3] = 'del'
193         cleanup.append(lambda: subprocess.call(args))
194
195     try:
196         subnet = network + prefix
197         my_ip = '%s/%s' % (utils.ipFromBin(subnet, '1'), len(subnet))
198         my_subnet = '%s/%u' % (utils.ipFromBin(subnet), len(subnet))
199         my_network = "%s/%u" % (utils.ipFromBin(network), len(network))
200         os.environ['re6stnet_ip'] = my_ip
201         os.environ['re6stnet_iface'] = config.main_interface
202         os.environ['re6stnet_subnet'] = my_subnet
203         os.environ['re6stnet_network'] = my_network
204
205         # Init db and tunnels
206         tunnel_interfaces = server_tunnels.keys()
207         timeout = 4 * config.hello
208         if config.client_count and not config.client:
209             required('registry')
210             # Create and open read_only pipe to get server events
211             r_pipe, write_pipe = os.pipe()
212             read_pipe = os.fdopen(r_pipe)
213             peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
214             tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db,
215                 config.openvpn_args, timeout, config.tunnel_refresh,
216                 config.client_count, config.iface_list, network, prefix,
217                 address, ip_changed, config.encrypt, remote_gateway)
218             tunnel_interfaces += tunnel_manager.new_iface_list
219         else:
220             tunnel_manager = write_pipe = None
221
222         cleanup = []
223         try:
224             # Source address selection is defined by RFC 6724, and in most
225             # applications, it usually works  thanks to rule 5 (prefer outgoing
226             # interface). But here, it rarely applies because we use several
227             # interfaces to connect to a re6st network.
228             # Rule 7 is little strange because it prefers temporary addresses
229             # over IP with a longer matching prefix (rule 8, which is not even
230             # mandatory).
231             # So only rule 6 can make the difference, i.e. prefer same label.
232             # The value of the label does not matter, except that it must be
233             # different from ::/0's (normally equal to 1).
234             # XXX: This does not work with extra interfaces that already have
235             #      an public IP so Babel must be changed to set a source
236             #      address on routes it installs.
237             ip('addrlabel', 'prefix', my_network, 'label', '99')
238             # prepare persistent interfaces
239             if config.client:
240                 cleanup.append(plib.client('re6stnet',
241                     utils.parse_address(config.client),
242                     config.encrypt, '--ping-restart', str(timeout),
243                     *config.openvpn_args).stop)
244             elif server_tunnels:
245                 required('dh')
246                 for iface, (port, proto) in server_tunnels.iteritems():
247                     cleanup.append(plib.server(iface, config.max_clients,
248                         config.dh, write_pipe, port, proto, config.encrypt,
249                         '--ping-exit', str(timeout), *config.openvpn_args).stop)
250
251             ip('addr', my_ip, 'dev', config.main_interface)
252             if_rt = ['ip', '-6', 'route', 'del',
253                      'fe80::/64', 'dev', config.main_interface]
254             if config.main_interface == 'lo':
255                 # WKRD: Removed this useless route now, since the kernel does
256                 #       not even remove it on exit.
257                 subprocess.call(if_rt)
258             if_rt[4] = my_subnet
259             cleanup.append(lambda: subprocess.call(if_rt))
260             x = [my_network]
261             if config.table:
262                 x += 'table', str(config.table)
263                 try:
264                     ip('rule', 'from', *x)
265                 except EnvironmentError:
266                     logging.error("It seems that your kernel was compiled"
267                         " without support for source address based routing"
268                         " (CONFIG_IPV6_SUBTREES). Consider using --table=0"
269                         " option if you can't change your kernel.")
270                     raise
271                 ip('rule', 'to', *x)
272                 call(if_rt)
273                 if_rt += x[1:]
274                 call(if_rt[:3] + ['add', 'proto', 'static'] + if_rt[4:])
275             ip('route', 'unreachable', *x)
276
277             config.babel_args += config.iface_list
278             cleanup.append(plib.router(subnet, config.hello, config.table,
279                 os.path.join(config.log, 'babeld.log'),
280                 os.path.join(config.state, 'babeld.state'),
281                 config.babel_pidfile, tunnel_interfaces,
282                 *config.babel_args).stop)
283             if config.up:
284                 r = os.system(config.up)
285                 if r:
286                     sys.exit(r)
287             for cmd in config.daemon or ():
288                 cleanup.append(utils.Popen(cmd, shell=True).stop)
289
290             # main loop
291             if tunnel_manager is None:
292                 sys.exit(os.WEXITSTATUS(os.wait()[1]))
293             cleanup += tunnel_manager.delInterfaces, tunnel_manager.killAll
294             while True:
295                 next = tunnel_manager.next_refresh
296                 if forwarder:
297                     next = min(next, forwarder.next_refresh)
298                 r = [read_pipe, tunnel_manager.sock]
299                 try:
300                     r = select.select(r, [], [], max(0, next - time.time()))[0]
301                 except select.error as e:
302                     if e.args[0] != errno.EINTR:
303                         raise
304                     continue
305                 if read_pipe in r:
306                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
307                 if tunnel_manager.sock in r:
308                     tunnel_manager.handlePeerEvent()
309                 t = time.time()
310                 if t >= tunnel_manager.next_refresh:
311                     tunnel_manager.refresh()
312                 if forwarder and t >= forwarder.next_refresh:
313                     forwarder.refresh()
314         finally:
315             while cleanup:
316                 try:
317                     cleanup.pop()()
318                 except:
319                     pass
320     except sqlite3.Error:
321         logging.exception("Restarting with empty cache")
322         os.rename(db_path, db_path + '.bak')
323         try:
324             sys.exitfunc()
325         finally:
326             os.execvp(sys.argv[0], sys.argv)
327     except KeyboardInterrupt:
328         return 0
329     except Exception:
330         f = traceback.format_exception(*sys.exc_info())
331         logging.error('%s%s', f.pop(), ''.join(f))
332         sys.exit(1)
333
334 if __name__ == "__main__":
335     main()