Documentation
[re6stnet.git] / re6stnet
1 #!/usr/bin/env python
2 import argparse, atexit, errno, logging, os
3 import select, signal, sqlite3, sys, time, traceback
4 from re6st import plib, utils, db, tunnel
5
6 def ovpnArgs(optional_args, ca_path, cert_path, key_path):
7     # Treat openvpn arguments
8     if optional_args and optional_args[0] == "--":
9         del optional_args[0]
10     optional_args.append('--ca')
11     optional_args.append(ca_path)
12     optional_args.append('--cert')
13     optional_args.append(cert_path)
14     optional_args.append('--key')
15     optional_args.append(key_path)
16     return optional_args
17
18
19 def getConfig():
20     parser = utils.ArgParser(fromfile_prefix_chars='@',
21         description="Resilient virtual private network application.")
22     _ = parser.add_argument
23
24     _('--ip',
25         help="IP address advertised to other nodes. Special values:\n"
26              "- upnp: force autoconfiguration via UPnP\n"
27              "- any: ask peers our IP\n"
28              " (default: ask peers if UPnP fails)")
29     _('--registry', required=True, metavar='URL',
30         help="Public HTTP URL of the registry, for bootstrapping.")
31     _('-l', '--log', default='/var/log/re6stnet',
32         help="Path to the directory used for log files:\n"
33              "- re6stnet.log: log file of re6stnet itself\n"
34              "- babeld.log: log file of router\n"
35              "- <iface>.log: 1 file per spawned OpenVPN\n")
36     _('-s', '--state', default='/var/lib/re6stnet',
37         help="Path to re6stnet state directory:\n"
38              "- peers.db: cache of peer addresses\n"
39              "- babeld.state: see option -S of babeld\n")
40     _('-v', '--verbose', default=1, type=int, metavar='LEVEL',
41         help="Log level of re6stnet itself. 0 disables logging."
42              " Use SIGUSR1 to reopen log."
43              " See also --babel-verb and --verb for logs of spawned processes.")
44     _('-i', '--interface', action='append', dest='iface_list', default=[],
45         help="Extra interface for LAN discovery. Highly recommanded if there"
46              " are other re6st node on the same network segment.")
47
48     _ = parser.add_argument_group('routing').add_argument
49     _('--babel-pidfile', metavar='PID',
50         help="Specify a file to write our process id to"
51              " (option -I of Babel).")
52     _('--babel-verb', default=0, metavar='LEVEL',
53         help="Log level of Babel (option -d of Babel).")
54     _('--hello', type=int, default=15,
55         help="Hello interval in seconds, for both wired and wireless"
56              " connections. OpenVPN ping-exit option is set to 4 times the"
57              " hello interval. It takes between 3 and 4 times the"
58              " hello interval for Babel to re-establish connection with a"
59              " node for which the direct connection has been cut.")
60     _('-w', '--wireless', action='store_true',
61         help="Assume all interfaces are wireless (option -w of Babel).")
62
63     _ = parser.add_argument_group('tunnelling').add_argument
64     _('--encrypt', action='store_true',
65         help='Specify that tunnels should be encrypted.')
66     _('--pp', nargs=2, action='append', metavar=('PORT', 'PROTO'),
67         help="Port and protocol to be announced to other peers, ordered by"
68              " preference. For each protocol (either udp or tcp), start one"
69              " openvpn server on the first given port."
70              " (default: --pp 1194 udp --pp 1194 tcp)")
71     _('--dh', required=True,
72         help='File containing Diffie-Hellman parameters in .pem format')
73     _('--ca', required=True, help=parser._ca_help)
74     _('--cert', required=True,
75         help="Local peer's signed certificate in .pem format."
76              " Common name defines the allocated prefix in the network.")
77     _('--key', required=True,
78         help="Local peer's private key in .pem format.")
79     _('--connection-count', default=20, type=int,
80         help="Maximum number of accepted clients per OpenVPN server."
81              " Also represents the average number of tunnels to peers.")
82     _('--tunnel-refresh', default=300, type=int,
83         help="Interval in seconds between two tunnel refresh: the worst"
84              " tunnel is closed if the number of client tunnels has reached"
85              " its maximum number (half of connection-count).")
86
87     _('openvpn_args', nargs=argparse.REMAINDER,
88         help="Use pseudo-argument '--' to forward positional arguments as extra"
89              " arguments to both server and client OpenVPN subprocesses.")
90     return parser.parse_args()
91
92
93 def main():
94     # Get arguments
95     config = getConfig()
96     network = utils.networkFromCa(config.ca)
97     prefix = utils.binFromSubnet(utils.subnetFromCert(config.cert))
98     openvpn_args = ovpnArgs(config.openvpn_args, config.ca, config.cert,
99                                                  config.key)
100     # Set logging
101     utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
102
103     logging.trace("Configuration:\n%r", config)
104     utils.makedirs(config.state)
105     db_path = os.path.join(config.state, 'peers.db')
106     plib.log = tunnel.log = config.log
107
108     # Create and open read_only pipe to get server events
109     logging.info('Creating pipe for server events...')
110     r_pipe, write_pipe = os.pipe()
111     read_pipe = os.fdopen(r_pipe)
112
113     signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
114     signal.signal(signal.SIGTERM, lambda *args: sys.exit())
115
116     address = []
117     if config.pp:
118         pp = [(int(port), proto) for port, proto in config.pp]
119     else:
120         pp = (1194, 'udp'), (1194, 'tcp')
121     ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
122     forwarder = None
123     if config.ip == 'upnp' or not config.ip:
124         logging.info('Attempting automatic configuration via UPnP...')
125         try:
126             from re6st.upnpigd import Forwarder
127             forwarder = Forwarder()
128         except Exception, e:
129             if config.ip:
130                 raise
131             logging.info("%s: assume we are not NATed", e)
132         else:
133             atexit.register(forwarder.clear)
134             for port, proto in pp:
135                 ip, port = forwarder.addRule(port, proto)
136                 address.append((ip, str(port), proto))
137     elif config.ip != 'any':
138         address = ip_changed(config.ip)
139     if address:
140         ip_changed = None
141
142     try:
143         # Init db and tunnels
144         peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
145         tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args,
146             config.hello, config.tunnel_refresh, config.connection_count,
147             config.iface_list, network, prefix, address, ip_changed,
148             config.encrypt)
149
150         server_tunnels = {}
151         for x in pp:
152             server_tunnels.setdefault('re6stnet-' + x[1], x)
153         interface_list = list(tunnel_manager.free_interface_set) \
154                        + config.iface_list + server_tunnels.keys()
155         subnet = network + prefix
156         router = plib.router(network, utils.ipFromBin(subnet), len(subnet),
157             interface_list, config.wireless, config.hello, config.babel_verb,
158             config.babel_pidfile, os.path.join(config.state, 'babeld.state'))
159
160         # main loop
161         try:
162             server_process = []
163             for iface, (port, proto) in server_tunnels.iteritems():
164                 server_process.append(plib.server(iface,
165                     utils.ipFromBin(subnet, '1') if proto == pp[0][1] else None,
166                     len(network) + len(prefix),
167                     config.connection_count, config.dh, write_pipe, port,
168                     proto, config.hello, config.encrypt, *openvpn_args))
169             while True:
170                 next = tunnel_manager.next_refresh
171                 if forwarder:
172                     next = min(next, forwarder.next_refresh)
173                 r = [read_pipe, tunnel_manager.sock]
174                 try:
175                     r = select.select(r, [], [], max(0, next - time.time()))[0]
176                 except select.error as e:
177                     if e.args[0] != errno.EINTR:
178                         raise
179                     continue
180                 if read_pipe in r:
181                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
182                 if tunnel_manager.sock in r:
183                     tunnel_manager.handlePeerEvent()
184                 t = time.time()
185                 if t >= tunnel_manager.next_refresh:
186                     tunnel_manager.refresh()
187                 if forwarder and t >= forwarder.next_refresh:
188                     forwarder.refresh()
189         finally:
190             router.terminate()
191             for p in server_process:
192                 try:
193                     p.kill()
194                 except:
195                     pass
196             try:
197                 tunnel_manager.killAll()
198             except:
199                 pass
200     except sqlite3.Error:
201         logging.exception("Restarting with empty cache")
202         os.rename(db_path, db_path + '.bak')
203         try:
204             sys.exitfunc()
205         finally:
206             os.execvp(sys.argv[0], sys.argv)
207     except KeyboardInterrupt:
208         return 0
209     except Exception:
210         f = traceback.format_exception(*sys.exc_info())
211         logging.error('%s%s', f.pop(), ''.join(f))
212         sys.exit(1)
213
214 if __name__ == "__main__":
215     main()