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