Accept multiple --ip arguments
[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', action='append', default=[],
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 len(config.ip) > 1 and 'upnp' in config.ip or 'any' in config.ip:
157             sys.exit("error: argument --ip can be given only once with 'any'"
158                      " or 'upnp' value")
159         if 'upnp' in config.ip or not config.ip:
160             logging.info('Attempting automatic configuration via UPnP...')
161             try:
162                 from re6st.upnpigd import Forwarder
163                 forwarder = Forwarder('re6stnet openvpn server')
164             except Exception, e:
165                 if config.ip:
166                     raise
167                 logging.info("%s: assume we are not NATed", e)
168             else:
169                 atexit.register(forwarder.clear)
170                 for port, proto in pp:
171                     forwarder.addRule(port, proto)
172                 ip_changed = forwarder.checkExternalIp
173                 address = ip_changed()
174         elif 'any' not in config.ip:
175             address = sum(map(ip_changed, config.ip), [])
176             ip_changed = None
177         for x in pp:
178             server_tunnels.setdefault('re6stnet-' + x[1], x)
179
180     def call(cmd):
181         logging.debug('%r', cmd)
182         p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
183                                   stderr=subprocess.PIPE)
184         stdout, stderr = p.communicate()
185         if p.returncode:
186             raise EnvironmentError("%r failed with error %u\n%s"
187                                    % (' '.join(cmd), p.returncode, stderr))
188         return stdout
189     def required(arg):
190         if not getattr(config, arg):
191             sys.exit("error: argument --%s is required" % arg)
192     def ip(object, *args):
193         args = ['ip', '-6', object, 'add'] + list(args)
194         call(args)
195         args[3] = 'del'
196         cleanup.append(lambda: subprocess.call(args))
197
198     try:
199         subnet = network + prefix
200         my_ip = '%s/%s' % (utils.ipFromBin(subnet, '1'), len(subnet))
201         my_subnet = '%s/%u' % (utils.ipFromBin(subnet), len(subnet))
202         my_network = "%s/%u" % (utils.ipFromBin(network), len(network))
203         os.environ['re6stnet_ip'] = my_ip
204         os.environ['re6stnet_iface'] = config.main_interface
205         os.environ['re6stnet_subnet'] = my_subnet
206         os.environ['re6stnet_network'] = my_network
207
208         # Init db and tunnels
209         tunnel_interfaces = server_tunnels.keys()
210         timeout = 4 * config.hello
211         if config.client_count and not config.client:
212             required('registry')
213             # Create and open read_only pipe to get server events
214             r_pipe, write_pipe = os.pipe()
215             read_pipe = os.fdopen(r_pipe)
216             peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
217             tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db,
218                 config.openvpn_args, timeout, config.tunnel_refresh,
219                 config.client_count, config.iface_list, network, prefix,
220                 address, ip_changed, config.encrypt, remote_gateway)
221             tunnel_interfaces += tunnel_manager.new_iface_list
222         else:
223             tunnel_manager = write_pipe = None
224
225         cleanup = []
226         try:
227             # Source address selection is defined by RFC 6724, and in most
228             # applications, it usually works  thanks to rule 5 (prefer outgoing
229             # interface). But here, it rarely applies because we use several
230             # interfaces to connect to a re6st network.
231             # Rule 7 is little strange because it prefers temporary addresses
232             # over IP with a longer matching prefix (rule 8, which is not even
233             # mandatory).
234             # So only rule 6 can make the difference, i.e. prefer same label.
235             # The value of the label does not matter, except that it must be
236             # different from ::/0's (normally equal to 1).
237             # XXX: This does not work with extra interfaces that already have
238             #      an public IP so Babel must be changed to set a source
239             #      address on routes it installs.
240             ip('addrlabel', 'prefix', my_network, 'label', '99')
241             # prepare persistent interfaces
242             if config.client:
243                 cleanup.append(plib.client('re6stnet',
244                     utils.parse_address(config.client),
245                     config.encrypt, '--ping-restart', str(timeout),
246                     *config.openvpn_args).stop)
247             elif server_tunnels:
248                 required('dh')
249                 for iface, (port, proto) in server_tunnels.iteritems():
250                     cleanup.append(plib.server(iface, config.max_clients,
251                         config.dh, write_pipe, port, proto, config.encrypt,
252                         '--ping-exit', str(timeout), *config.openvpn_args).stop)
253
254             ip('addr', my_ip, 'dev', config.main_interface)
255             if_rt = ['ip', '-6', 'route', 'del',
256                      'fe80::/64', 'dev', config.main_interface]
257             if config.main_interface == 'lo':
258                 # WKRD: Removed this useless route now, since the kernel does
259                 #       not even remove it on exit.
260                 subprocess.call(if_rt)
261             if_rt[4] = my_subnet
262             cleanup.append(lambda: subprocess.call(if_rt))
263             x = [my_network]
264             if config.table:
265                 x += 'table', str(config.table)
266                 try:
267                     ip('rule', 'from', *x)
268                 except EnvironmentError:
269                     logging.error("It seems that your kernel was compiled"
270                         " without support for source address based routing"
271                         " (CONFIG_IPV6_SUBTREES). Consider using --table=0"
272                         " option if you can't change your kernel.")
273                     raise
274                 ip('rule', 'to', *x)
275                 call(if_rt)
276                 if_rt += x[1:]
277                 call(if_rt[:3] + ['add', 'proto', 'static'] + if_rt[4:])
278             ip('route', 'unreachable', *x)
279
280             config.babel_args += config.iface_list
281             cleanup.append(plib.router(subnet, config.hello, config.table,
282                 os.path.join(config.log, 'babeld.log'),
283                 os.path.join(config.state, 'babeld.state'),
284                 config.babel_pidfile, tunnel_interfaces,
285                 *config.babel_args).stop)
286             if config.up:
287                 r = os.system(config.up)
288                 if r:
289                     sys.exit(r)
290             for cmd in config.daemon or ():
291                 cleanup.append(utils.Popen(cmd, shell=True).stop)
292
293             # main loop
294             if tunnel_manager is None:
295                 sys.exit(os.WEXITSTATUS(os.wait()[1]))
296             cleanup += tunnel_manager.delInterfaces, tunnel_manager.killAll
297             while True:
298                 next = tunnel_manager.next_refresh
299                 if forwarder:
300                     next = min(next, forwarder.next_refresh)
301                 r = [read_pipe, tunnel_manager.sock]
302                 try:
303                     r = select.select(r, [], [], max(0, next - time.time()))[0]
304                 except select.error as e:
305                     if e.args[0] != errno.EINTR:
306                         raise
307                     continue
308                 if read_pipe in r:
309                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
310                 if tunnel_manager.sock in r:
311                     tunnel_manager.handlePeerEvent()
312                 t = time.time()
313                 if t >= tunnel_manager.next_refresh:
314                     tunnel_manager.refresh()
315                 if forwarder and t >= forwarder.next_refresh:
316                     forwarder.refresh()
317         finally:
318             while cleanup:
319                 try:
320                     cleanup.pop()()
321                 except:
322                     pass
323     except sqlite3.Error:
324         logging.exception("Restarting with empty cache")
325         os.rename(db_path, db_path + '.bak')
326         try:
327             sys.exitfunc()
328         finally:
329             os.execvp(sys.argv[0], sys.argv)
330     except KeyboardInterrupt:
331         return 0
332     except Exception:
333         f = traceback.format_exception(*sys.exc_info())
334         logging.error('%s%s', f.pop(), ''.join(f))
335         sys.exit(1)
336
337 if __name__ == "__main__":
338     main()