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