Simplify by setting re6st IP on loopback interface by default
[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: force autoconfiguration via UPnP\n"
15              "- any: ask peers our IP\n"
16              " (default: ask peers if UPnP fails)")
17     _('--registry', metavar='URL',
18         help="Public HTTP URL of the registry, for bootstrapping.")
19     _('-l', '--log', default='/var/log/re6stnet',
20         help="Path to the directory used for log files:\n"
21              "- re6stnet.log: log file of re6stnet itself\n"
22              "- babeld.log: log file of router\n"
23              "- <iface>.log: 1 file per spawned OpenVPN\n")
24     _('-s', '--state', default='/var/lib/re6stnet',
25         help="Path to re6stnet state directory:\n"
26              "- peers.db: cache of peer addresses\n"
27              "- babeld.state: see option -S of babeld\n")
28     _('-v', '--verbose', default=1, type=int, metavar='LEVEL',
29         help="Log level of re6stnet itself. 0 disables logging."
30              " Use SIGUSR1 to reopen log."
31              " See also --babel-verb and --verb for logs of spawned processes.")
32     _('-i', '--interface', action='append', dest='iface_list', default=[],
33         help="Extra interface for LAN discovery. Highly recommanded if there"
34              " are other re6st node on the same network segment.")
35     _('-I', '--main-interface', metavar='IFACE', default='lo',
36         help="Set re6stnet IP on given interface. Any interface not used for"
37              " tunnelling can be chosen.")
38
39     _ = parser.add_argument_group('routing').add_argument
40     _('-B', dest='babel_args', metavar='ARG', action='append', default=[],
41         help="Extra arguments to forward to Babel.")
42     _('--babel-pidfile', metavar='PID', default='/var/run/re6st-babeld.pid',
43         help="Specify a file to write our process id to"
44              " (option -I of Babel).")
45     _('--hello', type=int, default=15,
46         help="Hello interval in seconds, for both wired and wireless"
47              " connections. OpenVPN ping-exit option is set to 4 times the"
48              " hello interval. It takes between 3 and 4 times the"
49              " hello interval for Babel to re-establish connection with a"
50              " node for which the direct connection has been cut.")
51
52     _ = parser.add_argument_group('tunnelling').add_argument
53     _('-O', dest='openvpn_args', metavar='ARG', action='append', default=[],
54         help="Extra arguments to forward to both server and client OpenVPN"
55              " subprocesses. Often used to configure verbosity.")
56     _('--ovpnlog', action='store_true',
57         help="Tell each OpenVPN subprocess to log to a dedicated file.")
58     _('--encrypt', action='store_true',
59         help='Specify that tunnels should be encrypted.')
60     _('--pp', nargs=2, action='append', metavar=('PORT', 'PROTO'),
61         help="Port and protocol to be announced to other peers, ordered by"
62              " preference. For each protocol (either udp or tcp), start one"
63              " openvpn server on the first given port."
64              " (default: --pp 1194 udp --pp 1194 tcp)")
65     _('--dh',
66         help='File containing Diffie-Hellman parameters in .pem format')
67     _('--ca', required=True, help=parser._ca_help)
68     _('--cert', required=True,
69         help="Local peer's signed certificate in .pem format."
70              " Common name defines the allocated prefix in the network.")
71     _('--key', required=True,
72         help="Local peer's private key in .pem format.")
73     _('--client-count', default=10, type=int,
74         help="Number of client tunnels to set up.")
75     _('--max-clients', type=int,
76         help="Maximum number of accepted clients per OpenVPN server. (default:"
77              " client-count * 2, which actually represents the average number"
78              " of tunnels to other peers)")
79     _('--tunnel-refresh', default=300, type=int,
80         help="Interval in seconds between two tunnel refresh: the worst"
81              " tunnel is closed if the number of client tunnels has reached"
82              " its maximum number (client-count).")
83     _('--client', metavar='HOST,PORT,PROTO[;...]',
84         help="Do not run any OpenVPN server, but only 1 OpenVPN client,"
85              " with specified remotes. Any other option not required in this"
86              " mode is ignored (e.g. client-count, max-clients, etc.)")
87
88     return parser.parse_args()
89
90
91 def main():
92     # Get arguments
93     config = getConfig()
94     network = utils.networkFromCa(config.ca)
95     prefix = utils.binFromSubnet(utils.subnetFromCert(config.cert))
96     config.openvpn_args += (
97         '--ca', config.ca,
98         '--cert', config.cert,
99         '--key', config.key)
100     # TODO: verify certificates (should we moved to M2Crypto ?)
101
102     # Set logging
103     utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
104
105     logging.trace("Configuration:\n%r", config)
106     utils.makedirs(config.state)
107     db_path = os.path.join(config.state, 'peers.db')
108     if config.ovpnlog:
109         plib.ovpn_log = config.log
110
111     signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
112     signal.signal(signal.SIGTERM, lambda *args: sys.exit())
113
114     if config.max_clients is None:
115         config.max_clients = config.client_count * 2
116
117     address = []
118     server_tunnels = {}
119     if config.client:
120         config.babel_args.append('re6stnet')
121     elif config.max_clients:
122         if config.pp:
123             pp = [(int(port), proto) for port, proto in config.pp]
124         else:
125             pp = (1194, 'udp'), (1194, 'tcp')
126         ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
127         forwarder = None
128         if config.ip == 'upnp' or not config.ip:
129             logging.info('Attempting automatic configuration via UPnP...')
130             try:
131                 from re6st.upnpigd import Forwarder
132                 forwarder = Forwarder()
133             except Exception, e:
134                 if config.ip:
135                     raise
136                 logging.info("%s: assume we are not NATed", e)
137             else:
138                 atexit.register(forwarder.clear)
139                 for port, proto in pp:
140                     ip, port = forwarder.addRule(port, proto)
141                     address.append((ip, str(port), proto))
142         elif config.ip != 'any':
143             address = ip_changed(config.ip)
144         if address:
145             ip_changed = None
146         for x in pp:
147             server_tunnels.setdefault('re6stnet-' + x[1], x)
148
149     def required(arg):
150         if not getattr(config, arg):
151             sys.exit("error: argument --%s is required" % arg)
152     def ip(object, *args):
153         args = ['ip', object, 'add'] + list(args)
154         r = subprocess.call(args)
155         if r:
156             sys.exit(r)
157         args[2] = 'del'
158         cleanup.append(lambda: subprocess.call(args))
159
160     try:
161         subnet = network + prefix
162         my_ip = '%s/%s' % (utils.ipFromBin(subnet, '1'), len(subnet))
163
164         # Init db and tunnels
165         tunnel_interfaces = server_tunnels.keys()
166         timeout = 4 * config.hello
167         if config.client_count and not config.client:
168             required('registry')
169             # Create and open read_only pipe to get server events
170             r_pipe, write_pipe = os.pipe()
171             read_pipe = os.fdopen(r_pipe)
172             peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
173             tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db,
174                 config.openvpn_args, timeout, config.tunnel_refresh,
175                 config.client_count, config.iface_list, network, prefix,
176                 address, ip_changed, config.encrypt)
177             tunnel_interfaces += tunnel_manager.free_interface_set
178         else:
179             tunnel_manager = write_pipe = None
180
181         config.babel_args += config.iface_list
182         cleanup = [plib.router(network, subnet, config.hello,
183             os.path.join(config.log, 'babeld.log'),
184             os.path.join(config.state, 'babeld.state'),
185             config.babel_pidfile, tunnel_interfaces,
186             *config.babel_args).terminate]
187
188         try:
189             my_network = "%s/%u" % (utils.ipFromBin(network), len(network))
190             ip('route', 'unreachable', my_network, 'proto', 'static')
191             # Source address selection is defined by RFC 3484, and in most
192             # applications, it usually works  thanks to rule 5 (prefer outgoing
193             # interface). But here, it rarely applies because we use several
194             # interfaces to connect to a re6st network.
195             # Rule 7 is little strange because it prefers temporary addresses
196             # over IP with a longer matching prefix (rule 8, which is not even
197             # mandatory).
198             # So only rule 6 can make the difference, i.e. prefer same label.
199             # The value of the label does not matter, except that it must be
200             # different from ::/0's (normally equal to 1).
201             # XXX: This does not work with extra interfaces that already have
202             #      an public IP so Babel must be changed to set a source
203             #      address on routes it installs.
204             ip('addrlabel', 'prefix', my_network, 'label', '99')
205             # prepare persistent interfaces
206             if config.client:
207                 cleanup.append(plib.client('re6stnet', config.client,
208                     config.encrypt, '--ping-restart', str(timeout),
209                     *config.openvpn_args).kill)
210             elif server_tunnels:
211                 required('dh')
212                 for iface, (port, proto) in server_tunnels.iteritems():
213                     cleanup.append(plib.server(iface, config.max_clients,
214                         config.dh, write_pipe, port, proto, config.encrypt,
215                         '--ping-exit', str(timeout), *config.openvpn_args).kill)
216
217             ip('addr', my_ip, 'dev', config.main_interface)
218             if config.main_interface == 'lo':
219                 # WKRD: The kernel does not remove these routes on exit.
220                 #       The first one can be removed now.
221                 del_rtr = ['ip', 'route', 'del', 'unreachable', 'fe80::/64',
222                            'dev', 'lo']
223                 subprocess.call(del_rtr)
224                 del_rtr[4] = '%s/%u' % (utils.ipFromBin(subnet), len(subnet))
225                 cleanup.append(lambda: subprocess.call(del_rtr))
226
227             # main loop
228             if tunnel_manager is None:
229                 sys.exit(os.WEXITSTATUS(os.wait()[1]))
230             cleanup.append(tunnel_manager.killAll)
231             while True:
232                 next = tunnel_manager.next_refresh
233                 if forwarder:
234                     next = min(next, forwarder.next_refresh)
235                 r = [read_pipe, tunnel_manager.sock]
236                 try:
237                     r = select.select(r, [], [], max(0, next - time.time()))[0]
238                 except select.error as e:
239                     if e.args[0] != errno.EINTR:
240                         raise
241                     continue
242                 if read_pipe in r:
243                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
244                 if tunnel_manager.sock in r:
245                     tunnel_manager.handlePeerEvent()
246                 t = time.time()
247                 if t >= tunnel_manager.next_refresh:
248                     tunnel_manager.refresh()
249                 if forwarder and t >= forwarder.next_refresh:
250                     forwarder.refresh()
251         finally:
252             for cleanup in cleanup:
253                 try:
254                     cleanup()
255                 except:
256                     pass
257     except sqlite3.Error:
258         logging.exception("Restarting with empty cache")
259         os.rename(db_path, db_path + '.bak')
260         try:
261             sys.exitfunc()
262         finally:
263             os.execvp(sys.argv[0], sys.argv)
264     except KeyboardInterrupt:
265         return 0
266     except Exception:
267         f = traceback.format_exception(*sys.exc_info())
268         logging.error('%s%s', f.pop(), ''.join(f))
269         sys.exit(1)
270
271 if __name__ == "__main__":
272     main()