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