Bugfixes, cleanup and improvements
[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     # General Configuration options
25     _('--ip',
26             help='IP address advertised to other nodes')
27     _('--registry', required=True,
28             help="HTTP URL of the discovery peer server,"
29                  " with public host (default port: 80)")
30     _('-l', '--log', default='/var/log/re6stnet',
31             help='Path to re6stnet logs directory')
32     _('-s', '--state', default='/var/lib/re6stnet',
33             help='Path to re6stnet state directory')
34     _('-v', '--verbose', default=1, type=int,
35             help='Log level of re6stnet itself')
36     _('-i', '--interface', action='append', dest='iface_list', default=[],
37             help='Extra interface for LAN discovery')
38
39     # Routing algorithm options
40     _('--babel-pidfile',
41             help='Specify a file to write our process id to')
42     _('--babel-verb', default=0,
43             help='Babel verbosity')
44     _('--hello', type=int, default=15,
45             help='Hello interval for babel, in seconds')
46     _('-w', '--wireless', action='store_true',
47             help='''Set all interfaces to be treated as wireless interfaces
48                     for the routing protocol''')
49
50     # Tunnel options
51     _('--encrypt', action='store_true',
52             help='specify that tunnels should be encrypted')
53     _('--pp', nargs=2, action='append',
54             help='Port and protocol to be used by other peers to connect')
55     _('--dh', required=True,
56             help='Path to dh file')
57     _('--ca', required=True,
58             help='Path to the certificate authority file')
59     _('--cert', required=True,
60             help='Path to the certificate file')
61     _('--key', required=True,
62             help='Path to the private key file')
63     _('--connection-count', default=20, type=int,
64             help='Number of tunnels')
65     _('--tunnel-refresh', default=300, type=int,
66             help='time (seconds) to wait before changing the connections')
67
68     # Openvpn options
69     _('openvpn_args', nargs=argparse.REMAINDER,
70             help="Common OpenVPN options")
71     return parser.parse_args()
72
73
74 def main():
75     # Get arguments
76     config = getConfig()
77     network = utils.networkFromCa(config.ca)
78     prefix = utils.binFromSubnet(utils.subnetFromCert(config.cert))
79     openvpn_args = ovpnArgs(config.openvpn_args, config.ca, config.cert,
80                                                  config.key)
81     # Set logging
82     utils.setupLog(config.verbose, os.path.join(config.log, 're6stnet.log'))
83
84     logging.trace("Configuration:\n%r", config)
85     utils.makedirs(config.state)
86     db_path = os.path.join(config.state, 'peers.db')
87     plib.log = tunnel.log = config.log
88
89     # Create and open read_only pipe to get server events
90     logging.info('Creating pipe for server events...')
91     r_pipe, write_pipe = os.pipe()
92     read_pipe = os.fdopen(r_pipe)
93
94     signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
95     signal.signal(signal.SIGTERM, lambda *args: sys.exit())
96
97     address = []
98     if config.pp:
99         pp = [(int(port), proto) for port, proto in config.pp]
100     else:
101         pp = (1194, 'udp'), (1194, 'tcp')
102     ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
103     forwarder = None
104     if config.ip == 'upnp' or not config.ip:
105         logging.info('Attempting automatic configuration via UPnP...')
106         try:
107             from re6st.upnpigd import Forwarder
108             forwarder = Forwarder()
109         except Exception, e:
110             if config.ip:
111                 raise
112             logging.info("%s: assume we are not NATed", e)
113         else:
114             atexit.register(forwarder.clear)
115             for port, proto in pp:
116                 ip, port = forwarder.addRule(port, proto)
117                 address.append((ip, str(port), proto))
118     elif config.ip != 'any':
119         address = ip_changed(config.ip)
120     if address:
121         ip_changed = None
122
123     try:
124         # Init db and tunnels
125         peer_db = db.PeerDB(db_path, config.registry, config.key, prefix)
126         tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args,
127             config.hello, config.tunnel_refresh, config.connection_count,
128             config.iface_list, network, prefix, address, ip_changed,
129             config.encrypt)
130
131         server_tunnels = {}
132         for x in pp:
133             server_tunnels.setdefault('re6stnet-' + x[1], x)
134         interface_list = list(tunnel_manager.free_interface_set) \
135                        + config.iface_list + server_tunnels.keys()
136         subnet = network + prefix
137         router = plib.router(network, utils.ipFromBin(subnet), len(subnet),
138             interface_list, config.wireless, config.hello, config.babel_verb,
139             config.babel_pidfile, os.path.join(config.state, 'babeld.state'))
140
141         # main loop
142         try:
143             server_process = []
144             for iface, (port, proto) in server_tunnels.iteritems():
145                 server_process.append(plib.server(iface,
146                     utils.ipFromBin(subnet, '1') if proto == pp[0][1] else None,
147                     len(network) + len(prefix),
148                     config.connection_count, config.dh, write_pipe, port,
149                     proto, config.hello, config.encrypt, *openvpn_args))
150             while True:
151                 next = tunnel_manager.next_refresh
152                 if forwarder:
153                     next = min(next, forwarder.next_refresh)
154                 r = [read_pipe, tunnel_manager.sock]
155                 try:
156                     r = select.select(r, [], [], max(0, next - time.time()))[0]
157                 except select.error as e:
158                     if e.args[0] != errno.EINTR:
159                         raise
160                     continue
161                 if read_pipe in r:
162                     tunnel_manager.handleTunnelEvent(read_pipe.readline())
163                 if tunnel_manager.sock in r:
164                     tunnel_manager.handlePeerEvent()
165                 t = time.time()
166                 if t >= tunnel_manager.next_refresh:
167                     tunnel_manager.refresh()
168                 if forwarder and t >= forwarder.next_refresh:
169                     forwarder.refresh()
170         finally:
171             router.terminate()
172             for p in server_process:
173                 try:
174                     p.kill()
175                 except:
176                     pass
177             try:
178                 tunnel_manager.killAll()
179             except:
180                 pass
181     except sqlite3.Error:
182         logging.exception("Restarting with empty cache")
183         os.rename(db_path, db_path + '.bak')
184         try:
185             sys.exitfunc()
186         finally:
187             os.execvp(sys.argv[0], sys.argv)
188     except KeyboardInterrupt:
189         return 0
190     except Exception:
191         f = traceback.format_exception(*sys.exc_info())
192         logging.error('%s%s', f.pop(), ''.join(f))
193         sys.exit(1)
194
195 if __name__ == "__main__":
196     main()