Merge branch 'master' of https://git.erp5.org/repos/re6stnet
[re6stnet.git] / re6stnet
1 #!/usr/bin/env python
2 import os, sys, select, time
3 import argparse, subprocess, sqlite3, logging, traceback
4 from argparse import ArgumentParser
5 from re6st import plib, utils, db, upnpigd, tunnel
6
7
8 class ArgParser(ArgumentParser):
9
10     def convert_arg_line_to_args(self, arg_line):
11         arg_line = arg_line.split('#')[0].rstrip()
12         if arg_line:
13             if arg_line.startswith('@'):
14                 yield arg_line
15                 return
16             for arg in ('--' + arg_line.lstrip('--')).split():
17                 if arg.strip():
18                     yield arg
19
20
21 def ovpnArgs(optional_args, ca_path, cert_path, key_path):
22     # Treat openvpn arguments
23     if optional_args and optional_args[0] == "--":
24         del optional_args[0]
25     optional_args.append('--ca')
26     optional_args.append(ca_path)
27     optional_args.append('--cert')
28     optional_args.append(cert_path)
29     optional_args.append('--key')
30     optional_args.append(key_path)
31     return optional_args
32
33
34 def getConfig():
35     parser = ArgParser(fromfile_prefix_chars='@',
36             description='Resilient virtual private network application')
37     _ = parser.add_argument
38
39     # General Configuration options
40     _('--ip', default=None, dest='address', action='append', nargs=3,
41             help='Ip address, port and protocol advertised to other vpn nodes')
42     _('--registry', required=True,
43             help="HTTP URL of the discovery peer server,"
44                  " with public host (default port: 80)")
45     _('--peers-db-refresh', default=3600, type=int,
46             help='the time (seconds) to wait before refreshing the peers db')
47     _('-l', '--log', default='/var/log',
48             help='Path to re6stnet logs directory')
49     _('-s', '--state', default='/var/lib/re6stnet',
50             help='Path to re6stnet state directory')
51     _('-v', '--verbose', default=0, type=int,
52             help='Defines the verbose level')
53     _('-i', '--interface', action='append', dest='iface_list', default=[],
54             help='Extra interface for LAN discovery')
55
56     # Routing algorithm options
57     _('--hello', type=int, default=15,
58             help='Hello interval for babel, in seconds')
59     _('-w', '--wireless', action='store_true',
60             help='''Set all interfaces to be treated as wireless interfaces
61                     for the routing protocol''')
62
63     # Tunnel options
64     _('--encrypt', action='store_true',
65             help='specify that tunnels should be encrypted')
66     _('--pp', nargs=2, action='append',
67             help='Port and protocol to be used by other peers to connect')
68     _('--dh', required=True,
69             help='Path to dh file')
70     _('--ca', required=True,
71             help='Path to the certificate authority file')
72     _('--cert', required=True,
73             help='Path to the certificate file')
74     _('--key', required=True,
75             help='Path to the private key file')
76     _('--connection-count', default=20, type=int,
77             help='Number of tunnels')
78     _('--tunnel-refresh', default=300, type=int,
79             help='time (seconds) to wait before changing the connections')
80
81     # Openvpn options
82     _('openvpn_args', nargs=argparse.REMAINDER,
83             help="Common OpenVPN options")
84     return parser.parse_args()
85
86
87 def main():
88     # Get arguments
89     config = getConfig()
90     if not config.pp:
91         config.pp = [['1194', 'udp'], ['1194', 'tcp-server']]
92     config.pp = list((port, proto, 're6stnet-%s' % proto)
93             for port, proto in config.pp)
94     manual = bool(config.address)
95     network = utils.networkFromCa(config.ca)
96     internal_ip, prefix = utils.ipFromCert(network, config.cert)
97     openvpn_args = ovpnArgs(config.openvpn_args, config.ca, config.cert,
98                                                  config.key)
99     db_path = os.path.join(config.state, 'peers.db')
100
101     # Set logging
102     utils.setupLog(config.verbose)
103
104     logging.trace("Configuration :\n%s" % config)
105
106     # Set global variables
107     tunnel.log = config.log
108     plib.verbose = config.verbose
109
110     # Create and open read_only pipe to get server events
111     logging.info('Creating pipe for server events...')
112     r_pipe, write_pipe = os.pipe()
113     read_pipe = os.fdopen(r_pipe)
114     logging.debug('Pipe created')
115
116     # Init db and tunnels
117     forwarder = None
118     if manual:
119         logging.info('Detected manual external configuration')
120         for c, s in ('udp', 'udp'), ('tcp-client', 'tcp-server'):
121             if len(list(x for x in config.address if x[2] == c)) \
122              < len(list(x for x in config.pp if x[1] == s)):
123                 logging.warning("""Beware: in manual configuration, you
124                         declared less external configurations regarding
125                         protocol %s/%s than you gave internal server
126                         configurations""" % (c, s))
127     else:
128         logging.info('Attempting automatic configuration via UPnP...')
129         try:
130             forwarder = upnpigd.Forwarder()
131             config.address = []
132             for port, proto, _ in config.pp:
133                 ext = forwarder.AddRule(port, proto)
134                 if ext:
135                     config.address.append(ext)
136         except upnpigd.NoUPnPDevice:
137             logging.info('No upnp device found')
138
139     peer_db = db.PeerManager(db_path, config.registry, config.key,
140             config.peers_db_refresh, config.address, internal_ip, prefix,
141             manual, config.pp, 200)
142     tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args,
143             config.hello, config.tunnel_refresh, config.connection_count,
144             config.iface_list, network, prefix, 2, config.encrypt)
145     peer_db.tunnel_manager = tunnel_manager
146
147     # Launch routing protocol. WARNING : you have to be root to start babeld
148     interface_list = list(tunnel_manager.free_interface_set) \
149                      + config.iface_list + list(iface
150                              for _, _, iface in config.pp)
151     router = plib.router(network, internal_ip, interface_list, config.wireless,
152             config.hello, os.path.join(config.state, 'babeld.state'),
153             stdout=os.open(os.path.join(config.log, 'babeld.log'),
154             os.O_WRONLY | os.O_CREAT | os.O_TRUNC), stderr=subprocess.STDOUT)
155
156    # Establish connections
157     server_process = []
158     server_ip = internal_ip
159     for port, proto, iface in config.pp:
160         server_process.append(plib.server(server_ip, len(network) + len(prefix),
161         config.connection_count, config.dh, write_pipe, port,
162         proto, config.hello, config.encrypt, '--dev', iface, *openvpn_args,
163         stdout=os.open(os.path.join(config.log,
164             're6stnet.server.%s.log' % (proto,)),
165             os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
166         stderr=subprocess.STDOUT))
167         server_ip = ''
168
169     # main loop
170     try:
171         try:
172             while True:
173                 logging.info('Sleeping ...')
174                 nextUpdate = min(tunnel_manager.next_refresh, peer_db.next_refresh)
175                 if forwarder != None:
176                     nextUpdate = min(nextUpdate, forwarder.next_refresh)
177                 nextUpdate = max(0, nextUpdate - time.time())
178                 select_list = [read_pipe]
179                 if peer_db.socket_file:
180                     select_list.append(peer_db.socket_file)
181                 ready, tmp1, tmp2 = select.select(select_list, [], [], nextUpdate)
182                 if read_pipe in ready:
183                     peer_db.handle_message(read_pipe.readline())
184                 if time.time() >= peer_db.next_refresh:
185                     peer_db.refresh()
186                 if time.time() >= tunnel_manager.next_refresh:
187                     tunnel_manager.refresh()
188                 if forwarder != None and time.time() > forwarder.next_refresh:
189                     forwarder.refresh()
190                 if peer_db.socket_file in ready:
191                     peer_db.readSocket()
192         finally:
193             for p in [router] + server_process:
194                 try:
195                     p.terminate()
196                 except:
197                     pass
198             try:
199                 tunnel_manager.killAll()
200             except:
201                 pass
202     except sqlite3.Error:
203         traceback.print_exc()
204         os.rename(db_path, db_path + '.bak')
205         os.execvp(sys.executable, sys.argv)
206     except KeyboardInterrupt:
207         return 0
208
209 if __name__ == "__main__":
210     main()