Server listen both ipv4 and ipv6
[re6stnet.git] / vifibnet.py
1 #!/usr/bin/env python
2 import argparse, errno, math, os, select, sqlite3, subprocess, sys, time, xmlrpclib
3 from OpenSSL import crypto
4 import traceback
5 import upnpigd
6 import openvpn
7 import random
8 import log
9
10 connection_dict = {} # to remember current connections we made
11 free_interface_set = set(('client1', 'client2', 'client3', 'client4', 'client5',
12 'client6', 'client7', 'client8', 'client9', 'client10'))
13
14 # TODO: flag in some way the peers that are connected to us so we don't connect to them
15 # Or maybe we just don't care
16
17 class PeersDB:
18 def __init__(self, dbPath):
19
20 log.log('Connectiong to peers database', 4)
21 self.db = sqlite3.connect(dbPath, isolation_level=None)
22 log.log('Preparing peers database', 4)
23 try:
24 self.db.execute("UPDATE peers SET used = 0")
25 except sqlite3.OperationalError, e:
26 if e.args[0] == 'no such table: peers':
27 raise RuntimeError
28
29 def populate(self, n):
30 # TODO: don't reconnect to server each time ?
31 log.log('Connecting to remote server', 3)
32 self.proxy = xmlrpclib.ServerProxy('http://%s:%u' % (config.server, config.server_port))
33 log.log('Updating peers database : populating', 2)
34 # TODO: determine port and proto
35 port = 1194
36 proto = 'udp'
37 new_peer_list = self.proxy.getPeerList(n, (config.internal_ip, config.external_ip, port, proto))
38 self.db.executemany("INSERT OR REPLACE INTO peers (ip, port, proto) VALUES (?,?,?)", new_peer_list)
39 self.db.execute("DELETE FROM peers WHERE ip = ?", (config.external_ip,))
40
41 def getUnusedPeers(self, nPeers):
42 return self.db.execute("SELECT id, ip, port, proto FROM peers WHERE used = 0 "
43 "ORDER BY RANDOM() LIMIT ?", (nPeers,))
44
45 def usePeer(self, id):
46 log.log('Updating peers database : using peer ' + str(id), 5)
47 self.db.execute("UPDATE peers SET used = 1 WHERE id = ?", (id,))
48
49 def unusePeer(self, id):
50 log.log('Updating peers database : unusing peer ' + str(id), 5)
51 self.db.execute("UPDATE peers SET used = 0 WHERE id = ?", (id,))
52
53 def ipFromBin(prefix):
54 prefix = hex(int(prefix, 2))[2:]
55 ip = ''
56 for i in xrange(0, len(prefix) - 1, 4):
57 ip += prefix[i:i+4] + ':'
58 return ip.rstrip(':')
59
60 def ipFromPrefix(prefix, prefix_len):
61 prefix = bin(int(prefix))[2:].rjust(prefix_len, '0')
62 ip_t = (config.vifibnet + prefix).ljust(128, '0')
63 return ipFromBin(ip_t)
64
65 def startBabel(**kw):
66 args = ['babeld',
67 '-C', 'redistribute local ip %s' % (config.internal_ip),
68 '-C', 'redistribute local deny',
69 # Route VIFIB ip adresses
70 '-C', 'in ip %s::/%u' % (ipFromBin(config.vifibnet), len(config.vifibnet)),
71 # Route only addresse in the 'local' network,
72 # or other entire networks
73 #'-C', 'in ip %s' % (config.internal_ip),
74 #'-C', 'in ip ::/0 le %s' % network_mask,
75 # Don't route other addresses
76 '-C', 'in deny',
77 '-d', str(config.verbose),
78 '-s',
79 ]
80 if config.babel_state:
81 args += '-S', config.babel_state
82 args = args + ['vifibnet'] + list(free_interface_set)
83 if config.verbose >= 5:
84 print args
85 return subprocess.Popen(args, **kw)
86
87 def getConfig():
88 global config
89 parser = argparse.ArgumentParser(
90 description='Resilient virtual private network application')
91 _ = parser.add_argument
92 # Server address MUST be a vifib address ( else requests will be denied )
93 _('--server', required=True,
94 help='Address for peer discovery server')
95 _('--server-port', required=True, type=int,
96 help='Peer discovery server port')
97 _('-l', '--log', default='/var/log',
98 help='Path to vifibnet logs directory')
99 _('--client-count', default=2, type=int,
100 help='Number of client connections')
101 # TODO: use maxpeer
102 _('--max-clients', default=10, type=int,
103 help='the number of peers that can connect to the server')
104 _('--refresh-time', default=60, type=int,
105 help='the time (seconds) to wait before changing the connections')
106 _('--refresh-count', default=1, type=int,
107 help='The number of connections to drop when refreshing the connections')
108 _('--db', default='/var/lib/vifibnet/peers.db',
109 help='Path to peers database')
110 _('--dh', required=True,
111 help='Path to dh file')
112 _('--babel-state', default='/var/lib/vifibnet/babel_state',
113 help='Path to babeld state-file')
114 _('--verbose', '-v', default=0, type=int,
115 help='Defines the verbose level')
116 _('--ca', required=True,
117 help='Path to the certificate authority file')
118 _('--cert', required=True,
119 help='Path to the certificate file')
120 _('--ip', required=True, dest='external_ip',
121 help='Ip address of the machine on the internet')
122 # Openvpn options
123 _('openvpn_args', nargs=argparse.REMAINDER,
124 help="Common OpenVPN options (e.g. certificates)")
125 openvpn.config = config = parser.parse_args()
126 log.verbose = config.verbose
127 # Get network prefix from ca.crt
128 with open(config.ca, 'r') as f:
129 ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
130 config.vifibnet = bin(ca.get_serial_number())[3:]
131 # Get ip from cert.crt
132 with open(config.cert, 'r') as f:
133 cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
134 subject = cert.get_subject()
135 prefix, prefix_len = subject.serialNumber.split('/')
136 config.internal_ip = ipFromPrefix(prefix, int(prefix_len))
137 log.log('Intranet ip : %s' % (config.internal_ip,), 3)
138 # Treat openvpn arguments
139 if config.openvpn_args[0] == "--":
140 del config.openvpn_args[0]
141 config.openvpn_args.append('--ca')
142 config.openvpn_args.append(config.ca)
143 config.openvpn_args.append('--cert')
144 config.openvpn_args.append(config.cert)
145
146 log.log("Configuration completed", 1)
147
148 def startNewConnection(n, write_pipe):
149 try:
150 for peer_id, ip, port, proto in peers_db.getUnusedPeers(n):
151 log.log('Establishing a connection with id %s (%s:%s)' % (peer_id, ip, port), 2)
152 iface = free_interface_set.pop()
153 connection_dict[peer_id] = ( openvpn.client( ip, write_pipe, '--dev', iface, '--proto', proto, '--rport', str(port),
154 stdout=os.open(os.path.join(config.log, 'vifibnet.client.%s.log' % (peer_id,)),
155 os.O_WRONLY|os.O_CREAT|os.O_TRUNC) ),
156 iface)
157 peers_db.usePeer(peer_id)
158 except KeyError:
159 log.log("Can't establish connection with %s : no available interface" % ip, 2)
160 except Exception:
161 traceback.print_exc()
162
163 def killConnection(peer_id):
164 try:
165 log.log('Killing the connection with id ' + str(peer_id), 2)
166 p, iface = connection_dict.pop(peer_id)
167 p.kill()
168 free_interface_set.add(iface)
169 peers_db.unusePeer(peer_id)
170 except KeyError:
171 log.log("Can't kill connection to " + peer_id + ": no existing connection", 1)
172 pass
173 except Exception:
174 log.log("Can't kill connection to " + peer_id + ": uncaught error", 1)
175 pass
176
177 def checkConnections():
178 for id in connection_dict.keys():
179 p, iface = connection_dict[id]
180 if p.poll() != None:
181 log.log('Connection with %s has failed with return code %s' % (id, p.returncode), 3)
182 free_interface_set.add(iface)
183 peers_db.unusePeer(id)
184 del connection_dict[id]
185
186 def refreshConnections(write_pipe):
187 checkConnections()
188 # Kill some random connections
189 try:
190 for i in range(0, max(0, len(connection_dict) - config.client_count + config.refresh_count)):
191 peer_id = random.choice(connection_dict.keys())
192 killConnection(peer_id)
193 except Exception:
194 pass
195 # Establish new connections
196 startNewConnection(config.client_count - len(connection_dict), write_pipe)
197
198 def handle_message(msg):
199 script_type, arg = msg.split()
200 if script_type == 'client-connect':
201 log.log('Incomming connection from %s' % (arg,), 3)
202 # TODO: check if we are not already connected to it
203 elif script_type == 'client-disconnect':
204 log.log('%s has disconnected' % (arg,), 3)
205 elif script_type == 'ipchange':
206 # TODO: save the external ip received
207 log.log('External Ip : ' + arg, 3)
208 else:
209 log.log('Unknow message recieved from the openvpn pipe : ' + msg, 1)
210
211 def main():
212 # Get arguments
213 getConfig()
214 log.verbose = config.verbose
215 # TODO: how do we decide which protocol we use ?
216 # (externalIp, externalPort) = upnpigd.GetExternalInfo(1194)
217
218 # Setup database
219 global peers_db # stop using global variables for everything ?
220 peers_db = PeersDB(config.db)
221
222 # Launch babel on all interfaces. WARNING : you have to be root to start babeld
223 log.log('Starting babel', 3)
224 babel = startBabel(stdout=os.open(os.path.join(config.log, 'vifibnet.babeld.log'), os.O_WRONLY | os.O_CREAT | os.O_TRUNC), stderr=subprocess.STDOUT)
225
226 # Create and open read_only pipe to get connect/disconnect events from openvpn
227 log.log('Creating pipe for openvpn events', 3)
228 r_pipe, write_pipe = os.pipe()
229 read_pipe = os.fdopen(r_pipe)
230
231 # Establish connections
232 log.log('Starting openvpn server', 3)
233 serverProcess = openvpn.server(config.internal_ip, write_pipe, '--dev', 'vifibnet',
234 stdout=os.open(os.path.join(config.log, 'vifibnet.server.log'), os.O_WRONLY | os.O_CREAT | os.O_TRUNC))
235 startNewConnection(config.client_count, write_pipe)
236
237 peers_db.populate(10)
238
239 # Timed refresh initializing
240 next_refresh = time.time() + config.refresh_time
241
242 # TODO: use peers_db.populate(100) every once in a while ?
243 # main loop
244 try:
245 while True:
246 ready, tmp1, tmp2 = select.select([read_pipe], [], [],
247 max(0, next_refresh - time.time()))
248 if ready:
249 handle_message(read_pipe.readline())
250 if time.time() >= next_refresh:
251 refreshConnections(write_pipe)
252 next_refresh = time.time() + config.refresh_time
253 except KeyboardInterrupt:
254 return 0
255
256 if __name__ == "__main__":
257 main()
258