re6stnet: new client-only and routing-only mode
[re6stnet.git] / re6stnet
1 #!/usr/bin/env 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
36     _ = parser.add_argument_group('routing').add_argument
37     _('-B', dest='babel_args', metavar='ARG', action='append', default=[],
38         help="Extra arguments to forward to Babel.")
39     _('--babel-pidfile', metavar='PID', default='/var/run/re6st-babeld.pid',
40         help="Specify a file to write our process id to"
41              " (option -I of Babel).")
42     _('--hello', type=int, default=15,
43         help="Hello interval in seconds, for both wired and wireless"
44              " connections. OpenVPN ping-exit option is set to 4 times the"
45              " hello interval. It takes between 3 and 4 times the"
46              " hello interval for Babel to re-establish connection with a"
47              " node for which the direct connection has been cut.")
48
49     _ = parser.add_argument_group('tunnelling').add_argument
50     _('-O', dest='openvpn_args', metavar='ARG', action='append', default=[],
51         help="Extra arguments to forward to both server and client OpenVPN"
52              " subprocesses. Often used to configure verbosity.")
53     _('--ovpnlog', action='store_true',
54         help="Tell each OpenVPN subprocess to log to a dedicated file.")
55     _('--encrypt', action='store_true',
56         help='Specify that tunnels should be encrypted.')
57     _('--pp', nargs=2, action='append', metavar=('PORT', 'PROTO'),
58         help="Port and protocol to be announced to other peers, ordered by"
59              " preference. For each protocol (either udp or tcp), start one"
60              " openvpn server on the first given port."
61              " (default: --pp 1194 udp --pp 1194 tcp)")
62     _('--dh',
63         help='File containing Diffie-Hellman parameters in .pem format')
64     _('--ca', required=True, help=parser._ca_help)
65     _('--cert', required=True,
66         help="Local peer's signed certificate in .pem format."
67              " Common name defines the allocated prefix in the network.")
68     _('--key', required=True,
69         help="Local peer's private key in .pem format.")
70     _('--client-count', default=10, type=int,
71         help="Number of client tunnels to set up.")
72     _('--max-clients', type=int,
73         help="Maximum number of accepted clients per OpenVPN server. (default:"
74              " client-count * 2, which actually represents the average number"
75              " of tunnels to other peers)")
76     _('--tunnel-refresh', default=300, type=int,
77         help="Interval in seconds between two tunnel refresh: the worst"
78              " tunnel is closed if the number of client tunnels has reached"
79              " its maximum number (client-count).")
80     _('--client', metavar='HOST,PORT,PROTO[;...]',
81         help="Do not run any OpenVPN server, but only 1 OpenVPN client,"
82              " with specified remotes. Any other option not required in this"
83              " mode is ignored (e.g. client-count, max-clients, etc.)")
84
85     return parser.parse_args()
86
87
88 def main():
89     # Get arguments
90     config = getConfig()
91     network = utils.networkFromCa(config.ca)
92     prefix = utils.binFromSubnet(utils.subnetFromCert(config.cert))
93     config.openvpn_args += (
94         '--ca', config.ca,
95         '--cert', config.cert,
96         '--key', config.key)
97     # TODO: verify certificates (should we moved to M2Crypto ?)
98
99     # Set logging
100     utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
101
102     logging.trace("Configuration:\n%r", config)
103     utils.makedirs(config.state)
104     db_path = os.path.join(config.state, 'peers.db')
105     if config.ovpnlog:
106         plib.ovpn_log = config.log
107
108     signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
109     signal.signal(signal.SIGTERM, lambda *args: sys.exit())
110
111     if config.max_clients is None:
112         config.max_clients = config.client_count * 2
113
114     address = []
115     server_tunnels = {}
116     if config.client:
117         config.babel_args.append('re6stnet')
118     elif config.max_clients:
119         if config.pp:
120             pp = [(int(port), proto) for port, proto in config.pp]
121         else:
122             pp = (1194, 'udp'), (1194, 'tcp')
123         ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
124         forwarder = None
125         if config.ip == 'upnp' or not config.ip:
126             logging.info('Attempting automatic configuration via UPnP...')
127             try:
128                 from re6st.upnpigd import Forwarder
129                 forwarder = Forwarder()
130             except Exception, e:
131                 if config.ip:
132                     raise
133                 logging.info("%s: assume we are not NATed", e)
134             else:
135                 atexit.register(forwarder.clear)
136                 for port, proto in pp:
137                     ip, port = forwarder.addRule(port, proto)
138                     address.append((ip, str(port), proto))
139         elif config.ip != 'any':
140             address = ip_changed(config.ip)
141         if address:
142             ip_changed = None
143         for x in pp:
144             server_tunnels.setdefault('re6stnet-' + x[1], x)
145         config.babel_args += server_tunnels
146
147     def call(*args, **kw):
148         r = subprocess.call(*args, **kw)
149         if r:
150             sys.exit(r)
151     def required(arg):
152         if not getattr(config, arg):
153             sys.exit("error: argument --%s is required" % arg)
154
155     try:
156         subnet = network + prefix
157         my_ip = '%s/%s' % (utils.ipFromBin(subnet, '1'), len(subnet))
158
159         # Init db and tunnels
160         if config.client_count and not config.client:
161             required('registry')
162             # Create and open read_only pipe to get server events
163             r_pipe, write_pipe = os.pipe()
164             read_pipe = os.fdopen(r_pipe)
165             peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
166             tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db,
167                 config.openvpn_args, config.hello, config.tunnel_refresh,
168                 config.client_count, config.iface_list, network, prefix,
169                 address, ip_changed, config.encrypt)
170             config.babel_args += tunnel_manager.free_interface_set
171         else:
172             tunnel_manager = write_pipe = None
173
174         config.babel_args += config.iface_list
175         router = plib.router(network, utils.ipFromBin(subnet), len(subnet),
176             config.hello, os.path.join(config.log, 'babeld.log'),
177             os.path.join(config.state, 'babeld.state'),
178             config.babel_pidfile, *config.babel_args)
179
180         try:
181             cleanup = []
182             # prepare persistent interfaces
183             if config.client:
184                 cleanup.append(plib.client('re6stnet', config.client,
185                     config.encrypt,
186                     '--up', plib.ovpn_server + ' ' + my_ip,
187                     '--ping-restart', str(4 * config.hello),
188                     *config.openvpn_args).kill)
189             elif server_tunnels:
190                 required('dh')
191                 for iface, (port, proto) in server_tunnels.iteritems():
192                     cleanup.append(plib.server(iface,
193                         my_ip if proto == pp[0][1] else None,
194                         config.max_clients, config.dh, write_pipe, port,
195                         proto, config.encrypt, *config.openvpn_args).kill)
196             elif config.iface_list:
197                 ip_args = ['ip', 'addr', 'add', my_ip,
198                            'dev', config.iface_list[0]]
199                 call(ip_args)
200                 ip_args[2] = 'del'
201                 cleanup.append(lambda: call(ip_args))
202             else:
203                 sys.exit("--client or --interface required"
204                          " when --max-clients is 0")
205
206             # main loop
207             if tunnel_manager is None:
208                 sys.exit(os.WEXITSTATUS(os.wait()[1]))
209             cleanup.append(tunnel_manager.killAll)
210             while True:
211                 next = tunnel_manager.next_refresh
212                 if forwarder:
213                     next = min(next, forwarder.next_refresh)
214                 r = [read_pipe, tunnel_manager.sock]
215                 try:
216                     r = select.select(r, [], [], max(0, next - time.time()))[0]
217                 except select.error as e:
218                     if e.args[0] != errno.EINTR:
219                         raise
220                     continue
221                 if read_pipe in r:
222                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
223                 if tunnel_manager.sock in r:
224                     tunnel_manager.handlePeerEvent()
225                 t = time.time()
226                 if t >= tunnel_manager.next_refresh:
227                     tunnel_manager.refresh()
228                 if forwarder and t >= forwarder.next_refresh:
229                     forwarder.refresh()
230         finally:
231             router.terminate()
232             for cleanup in cleanup:
233                 try:
234                     cleanup()
235                 except:
236                     pass
237     except sqlite3.Error:
238         logging.exception("Restarting with empty cache")
239         os.rename(db_path, db_path + '.bak')
240         try:
241             sys.exitfunc()
242         finally:
243             os.execvp(sys.argv[0], sys.argv)
244     except KeyboardInterrupt:
245         return 0
246     except Exception:
247         f = traceback.format_exception(*sys.exc_info())
248         logging.error('%s%s', f.pop(), ''.join(f))
249         sys.exit(1)
250
251 if __name__ == "__main__":
252     main()