Commit | Line | Data |
---|---|---|
2ec90437 MW |
1 | #! @PYTHON@ |
2 | ### -*-python-*- | |
3 | ### | |
4 | ### Service for automatically tracking network connection status | |
5 | ### | |
6 | ### (c) 2010 Straylight/Edgeware | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This file is part of Trivial IP Encryption (TrIPE). | |
12 | ### | |
11ad66c2 MW |
13 | ### TrIPE is free software: you can redistribute it and/or modify it under |
14 | ### the terms of the GNU General Public License as published by the Free | |
15 | ### Software Foundation; either version 3 of the License, or (at your | |
16 | ### option) any later version. | |
2ec90437 | 17 | ### |
11ad66c2 MW |
18 | ### TrIPE is distributed in the hope that it will be useful, but WITHOUT |
19 | ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
20 | ### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
21 | ### for more details. | |
2ec90437 MW |
22 | ### |
23 | ### You should have received a copy of the GNU General Public License | |
11ad66c2 | 24 | ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>. |
2ec90437 MW |
25 | |
26 | VERSION = '@VERSION@' | |
27 | ||
28 | ###-------------------------------------------------------------------------- | |
29 | ### External dependencies. | |
30 | ||
31 | from ConfigParser import RawConfigParser | |
32 | from optparse import OptionParser | |
33 | import os as OS | |
34 | import sys as SYS | |
35 | import socket as S | |
36 | import mLib as M | |
37 | import tripe as T | |
38 | import dbus as D | |
f8204e50 | 39 | import re as RX |
2ec90437 MW |
40 | for i in ['mainloop', 'mainloop.glib']: |
41 | __import__('dbus.%s' % i) | |
a69f4417 MW |
42 | try: from gi.repository import GLib as G |
43 | except ImportError: import gobject as G | |
2ec90437 | 44 | from struct import pack, unpack |
c21d936a | 45 | from cStringIO import StringIO |
2ec90437 MW |
46 | |
47 | SM = T.svcmgr | |
48 | ##__import__('rmcr').__debug = True | |
49 | ||
50 | ###-------------------------------------------------------------------------- | |
51 | ### Utilities. | |
52 | ||
53 | class struct (object): | |
54 | """A simple container object.""" | |
55 | def __init__(me, **kw): | |
56 | me.__dict__.update(kw) | |
57 | ||
c21d936a MW |
58 | def loadb(s): |
59 | n = 0 | |
60 | for ch in s: n = 256*n + ord(ch) | |
61 | return n | |
62 | ||
63 | def storeb(n, wd = None): | |
64 | if wd is None: wd = n.bit_length() | |
65 | s = StringIO() | |
66 | for i in xrange((wd - 1)&-8, -8, -8): s.write(chr((n >> i)&0xff)) | |
67 | return s.getvalue() | |
68 | ||
6e8bbeeb MW |
69 | ###-------------------------------------------------------------------------- |
70 | ### Address manipulation. | |
c21d936a MW |
71 | ### |
72 | ### I think this is the most demanding application, in terms of address | |
73 | ### hacking, in the entire TrIPE suite. At least we don't have to do it in | |
74 | ### C. | |
6e8bbeeb | 75 | |
c21d936a | 76 | class BaseAddress (object): |
6aa21132 | 77 | def __init__(me, addrstr, maskstr = None): |
c21d936a | 78 | me._setaddr(addrstr) |
6aa21132 MW |
79 | if maskstr is None: |
80 | me.mask = -1 | |
81 | elif maskstr.isdigit(): | |
c21d936a | 82 | me.mask = (1 << me.NBITS) - (1 << me.NBITS - int(maskstr)) |
6aa21132 | 83 | else: |
c21d936a | 84 | me._setmask(maskstr) |
6aa21132 MW |
85 | if me.addr&~me.mask: |
86 | raise ValueError('network contains bits set beyond mask') | |
87 | def _addrstr_to_int(me, addrstr): | |
c21d936a MW |
88 | try: return loadb(S.inet_pton(me.AF, addrstr)) |
89 | except S.error: raise ValueError('bad address syntax') | |
6aa21132 | 90 | def _int_to_addrstr(me, n): |
c21d936a MW |
91 | return S.inet_ntop(me.AF, storeb(me.addr, me.NBITS)) |
92 | def _setmask(me, maskstr): | |
93 | raise ValueError('only prefix masked supported') | |
94 | def _maskstr(me): | |
95 | raise ValueError('only prefix masked supported') | |
6aa21132 MW |
96 | def sockaddr(me, port = 0): |
97 | if me.mask != -1: raise ValueError('not a simple address') | |
c21d936a | 98 | return me._sockaddr(port) |
6aa21132 | 99 | def __str__(me): |
c21d936a | 100 | addrstr = me._addrstr() |
6aa21132 MW |
101 | if me.mask == -1: |
102 | return addrstr | |
103 | else: | |
c21d936a | 104 | inv = me.mask ^ ((1 << me.NBITS) - 1) |
6aa21132 | 105 | if (inv&(inv + 1)) == 0: |
c21d936a | 106 | return '%s/%d' % (addrstr, me.NBITS - inv.bit_length()) |
6aa21132 | 107 | else: |
c21d936a | 108 | return '%s/%s' % (addrstr, me._maskstr()) |
6aa21132 | 109 | def withinp(me, net): |
c21d936a | 110 | if type(net) != type(me): return False |
6aa21132 MW |
111 | if (me.mask&net.mask) != net.mask: return False |
112 | if (me.addr ^ net.addr)&net.mask: return False | |
c21d936a | 113 | return me._withinp(net) |
6aa21132 | 114 | def eq(me, other): |
c21d936a | 115 | if type(me) != type(other): return False |
6aa21132 MW |
116 | if me.mask != other.mask: return False |
117 | if me.addr != other.addr: return False | |
c21d936a MW |
118 | return me._eq(other) |
119 | def _withinp(me, net): | |
6aa21132 | 120 | return True |
c21d936a MW |
121 | def _eq(me, other): |
122 | return True | |
123 | ||
124 | class InetAddress (BaseAddress): | |
125 | AF = S.AF_INET | |
126 | AFNAME = 'IPv4' | |
127 | NBITS = 32 | |
128 | def _addrstr_to_int(me, addrstr): | |
129 | try: return loadb(S.inet_aton(addrstr)) | |
130 | except S.error: raise ValueError('bad address syntax') | |
131 | def _setaddr(me, addrstr): | |
132 | me.addr = me._addrstr_to_int(addrstr) | |
133 | def _setmask(me, maskstr): | |
134 | me.mask = me._addrstr_to_int(maskstr) | |
135 | def _addrstr(me): | |
136 | return me._int_to_addrstr(me.addr) | |
137 | def _maskstr(me): | |
138 | return me._int_to_addrstr(me.mask) | |
139 | def _sockaddr(me, port = 0): | |
140 | return (me._addrstr(), port) | |
6aa21132 MW |
141 | @classmethod |
142 | def from_sockaddr(cls, sa): | |
143 | addr, port = (lambda a, p: (a, p))(*sa) | |
144 | return cls(addr), port | |
145 | ||
fb78e73a MW |
146 | class Inet6Address (BaseAddress): |
147 | AF = S.AF_INET6 | |
148 | AFNAME = 'IPv6' | |
149 | NBITS = 128 | |
150 | def _setaddr(me, addrstr): | |
151 | pc = addrstr.find('%') | |
152 | if pc == -1: | |
153 | me.addr = me._addrstr_to_int(addrstr) | |
154 | me.scope = 0 | |
155 | else: | |
156 | me.addr = me._addrstr_to_int(addrstr[:pc]) | |
157 | ais = S.getaddrinfo(addrstr, 0, S.AF_INET6, S.SOCK_DGRAM, 0, | |
158 | S.AI_NUMERICHOST | S.AI_NUMERICSERV) | |
159 | me.scope = ais[0][4][3] | |
160 | def _addrstr(me): | |
161 | addrstr = me._int_to_addrstr(me.addr) | |
162 | if me.scope == 0: | |
163 | return addrstr | |
164 | else: | |
165 | name, _ = S.getnameinfo((addrstr, 0, 0, me.scope), | |
166 | S.NI_NUMERICHOST | S.NI_NUMERICSERV) | |
167 | return name | |
168 | def _sockaddr(me, port = 0): | |
169 | return (me._addrstr(), port, 0, me.scope) | |
170 | @classmethod | |
171 | def from_sockaddr(cls, sa): | |
172 | addr, port, _, scope = (lambda a, p, f = 0, s = 0: (a, p, f, s))(*sa) | |
173 | me = cls(addr) | |
174 | me.scope = scope | |
175 | return me, port | |
176 | def _withinp(me, net): | |
177 | return net.scope == 0 or me.scope == net.scope | |
178 | def _eq(me, other): | |
179 | return me.scope == other.scope | |
180 | ||
6aa21132 | 181 | def parse_address(addrstr, maskstr = None): |
fb78e73a MW |
182 | if addrstr.find(':') >= 0: return Inet6Address(addrstr, maskstr) |
183 | else: return InetAddress(addrstr, maskstr) | |
11ab0da6 | 184 | |
69bdcb64 MW |
185 | def parse_net(netstr): |
186 | try: sl = netstr.index('/') | |
187 | except ValueError: raise ValueError('missing mask') | |
6aa21132 | 188 | return parse_address(netstr[:sl], netstr[sl + 1:]) |
69bdcb64 | 189 | |
6aa21132 | 190 | def straddr(a): return a is None and '#<none>' or str(a) |
6e8bbeeb | 191 | |
2ec90437 MW |
192 | ###-------------------------------------------------------------------------- |
193 | ### Parse the configuration file. | |
194 | ||
195 | ## Hmm. Should I try to integrate this with the peers database? It's not a | |
196 | ## good fit; it'd need special hacks in tripe-newpeers. And the use case for | |
197 | ## this service are largely going to be satellite notes, I don't think | |
198 | ## scalability's going to be a problem. | |
199 | ||
fb78e73a | 200 | TESTADDRS = [InetAddress('1.2.3.4'), Inet6Address('2001::1')] |
6aa21132 | 201 | |
f8204e50 MW |
202 | CONFSYNTAX = [ |
203 | ('COMMENT', RX.compile(r'^\s*($|[;#])')), | |
204 | ('GRPHDR', RX.compile(r'^\s*\[(.*)\]\s*$')), | |
205 | ('ASSGN', RX.compile(r'\s*([\w.-]+)\s*[:=]\s*(|\S|\S.*\S)\s*$'))] | |
206 | ||
207 | class ConfigError (Exception): | |
208 | def __init__(me, file, lno, msg): | |
209 | me.file = file | |
210 | me.lno = lno | |
211 | me.msg = msg | |
212 | def __str__(me): | |
213 | return '%s:%d: %s' % (me.file, me.lno, me.msg) | |
214 | ||
2ec90437 MW |
215 | class Config (object): |
216 | """ | |
217 | Represents a configuration file. | |
218 | ||
219 | The most interesting thing is probably the `groups' slot, which stores a | |
220 | list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a | |
a59afe07 | 221 | list of (TAG, PEER, NETS) triples. The implication is that there should be |
6aa21132 | 222 | precisely one peer from the set, and that it should be named TAG, where |
a59afe07 | 223 | (TAG, PEER, NETS) is the first triple such that the host's primary IP |
6aa21132 | 224 | address (if PEER is None -- or the IP address it would use for |
a59afe07 | 225 | communicating with PEER) is within one of the networks defined by NETS. |
2ec90437 MW |
226 | """ |
227 | ||
228 | def __init__(me, file): | |
229 | """ | |
230 | Construct a new Config object, reading the given FILE. | |
231 | """ | |
232 | me._file = file | |
233 | me._fwatch = M.FWatch(file) | |
234 | me._update() | |
235 | ||
236 | def check(me): | |
237 | """ | |
238 | See whether the configuration file has been updated. | |
239 | """ | |
240 | if me._fwatch.update(): | |
241 | me._update() | |
242 | ||
243 | def _update(me): | |
244 | """ | |
245 | Internal function to update the configuration from the underlying file. | |
246 | """ | |
247 | ||
2d4998c4 | 248 | if T._debug: print '# reread config' |
2ec90437 | 249 | |
f8204e50 | 250 | ## Initial state. |
e152ccf2 | 251 | testaddrs = {} |
f8d6fc7b | 252 | groups = {} |
f8204e50 MW |
253 | grpname = None |
254 | grplist = [] | |
255 | ||
256 | ## Open the file and start reading. | |
257 | with open(me._file) as f: | |
258 | lno = 0 | |
259 | for line in f: | |
260 | lno += 1 | |
261 | for tag, rx in CONFSYNTAX: | |
262 | m = rx.match(line) | |
263 | if m: break | |
2ec90437 | 264 | else: |
f8204e50 MW |
265 | raise ConfigError(me._file, lno, 'failed to parse line: %r' % line) |
266 | ||
267 | if tag == 'COMMENT': | |
268 | ## A comment. Ignore it and hope it goes away. | |
269 | ||
270 | continue | |
271 | ||
272 | elif tag == 'GRPHDR': | |
273 | ## A group header. Flush the old group and start a new one. | |
274 | newname = m.group(1) | |
275 | ||
276 | if grpname is not None: groups[grpname] = grplist | |
277 | if newname in groups: | |
278 | raise ConfigError(me._file, lno, | |
279 | "duplicate group name `%s'" % newname) | |
280 | grpname = newname | |
281 | grplist = [] | |
282 | ||
283 | elif tag == 'ASSGN': | |
284 | ## An assignment. Deal with it. | |
285 | name, value = m.group(1), m.group(2) | |
286 | ||
287 | if grpname is None: | |
288 | ## We're outside of any group, so this is a global configuration | |
289 | ## tweak. | |
290 | ||
291 | if name == 'test-addr': | |
292 | for astr in value.split(): | |
293 | try: | |
294 | a = parse_address(astr) | |
295 | except Exception, e: | |
296 | raise ConfigError(me._file, lno, | |
297 | "invalid IP address `%s': %s" % | |
298 | (astr, e)) | |
e152ccf2 MW |
299 | if a.AF in testaddrs: |
300 | raise ConfigError(me._file, lno, | |
301 | 'duplicate %s test-address' % a.AFNAME) | |
302 | testaddrs[a.AF] = a | |
f8204e50 MW |
303 | else: |
304 | raise ConfigError(me._file, lno, | |
305 | "unknown global option `%s'" % name) | |
306 | ||
307 | else: | |
308 | ## Parse a pattern and add it to the group. | |
309 | spec = value.split() | |
310 | i = 0 | |
311 | ||
312 | ## Check for an explicit target address. | |
313 | if i >= len(spec) or spec[i].find('/') >= 0: | |
314 | peer = None | |
e152ccf2 | 315 | af = None |
f8204e50 MW |
316 | else: |
317 | try: | |
318 | peer = parse_address(spec[i]) | |
319 | except Exception, e: | |
320 | raise ConfigError(me._file, lno, | |
321 | "invalid IP address `%s': %s" % | |
322 | (spec[i], e)) | |
e152ccf2 | 323 | af = peer.AF |
f8204e50 MW |
324 | i += 1 |
325 | ||
a59afe07 MW |
326 | ## Parse the list of local networks. |
327 | nets = [] | |
328 | while i < len(spec): | |
329 | try: | |
330 | net = parse_net(spec[i]) | |
331 | except Exception, e: | |
332 | raise ConfigError(me._file, lno, | |
333 | "invalid IP network `%s': %s" % | |
334 | (spec[i], e)) | |
335 | else: | |
336 | nets.append(net) | |
337 | i += 1 | |
338 | if not nets: | |
339 | raise ConfigError(me._file, lno, 'no networks defined') | |
f8204e50 | 340 | |
e152ccf2 MW |
341 | ## Make sure that the addresses are consistent. |
342 | for net in nets: | |
343 | if af is None: | |
344 | af = net.AF | |
345 | elif net.AF != af: | |
346 | raise ConfigError(me._file, lno, | |
347 | "net %s doesn't match" % net) | |
348 | ||
f8204e50 | 349 | ## Add this entry to the list. |
a59afe07 | 350 | grplist.append((name, peer, nets)) |
f8204e50 | 351 | |
e152ccf2 MW |
352 | ## Fill in the default test addresses if necessary. |
353 | for a in TESTADDRS: testaddrs.setdefault(a.AF, a) | |
2ec90437 MW |
354 | |
355 | ## Done. | |
f8204e50 | 356 | if grpname is not None: groups[grpname] = grplist |
e152ccf2 | 357 | me.testaddrs = testaddrs |
2ec90437 MW |
358 | me.groups = groups |
359 | ||
360 | ### This will be a configuration file. | |
361 | CF = None | |
362 | ||
2d4998c4 | 363 | def cmd_showconfig(): |
e152ccf2 MW |
364 | T.svcinfo('test-addr=%s' % |
365 | ' '.join(str(a) | |
366 | for a in sorted(CF.testaddrs.itervalues(), | |
367 | key = lambda a: a.AFNAME))) | |
2d4998c4 | 368 | def cmd_showgroups(): |
f8d6fc7b MW |
369 | for g in sorted(CF.groups.iterkeys()): |
370 | T.svcinfo(g) | |
2d4998c4 | 371 | def cmd_showgroup(g): |
f8d6fc7b MW |
372 | try: pats = CF.groups[g] |
373 | except KeyError: raise T.TripeJobError('unknown-group', g) | |
a59afe07 | 374 | for t, p, nn in pats: |
2d4998c4 | 375 | T.svcinfo('peer', t, |
6aa21132 | 376 | 'target', p and str(p) or '(default)', |
a59afe07 | 377 | 'net', ' '.join(map(str, nn))) |
2d4998c4 | 378 | |
2ec90437 MW |
379 | ###-------------------------------------------------------------------------- |
380 | ### Responding to a network up/down event. | |
381 | ||
382 | def localaddr(peer): | |
383 | """ | |
384 | Return the local IP address used for talking to PEER. | |
385 | """ | |
e152ccf2 | 386 | sk = S.socket(peer.AF, S.SOCK_DGRAM) |
2ec90437 MW |
387 | try: |
388 | try: | |
6aa21132 MW |
389 | sk.connect(peer.sockaddr(1)) |
390 | addr = sk.getsockname() | |
e152ccf2 | 391 | return type(peer).from_sockaddr(addr)[0] |
2ec90437 MW |
392 | except S.error: |
393 | return None | |
394 | finally: | |
395 | sk.close() | |
396 | ||
397 | _kick = T.Queue() | |
f5393555 MW |
398 | _delay = None |
399 | ||
400 | def cancel_delay(): | |
401 | global _delay | |
402 | if _delay is not None: | |
403 | if T._debug: print '# cancel delayed kick' | |
404 | G.source_remove(_delay) | |
405 | _delay = None | |
4f6b41b9 MW |
406 | |
407 | def netupdown(upness, reason): | |
408 | """ | |
409 | Add or kill peers according to whether the network is up or down. | |
410 | ||
411 | UPNESS is true if the network is up, or false if it's down. | |
412 | """ | |
413 | ||
414 | _kick.put((upness, reason)) | |
415 | ||
f5393555 MW |
416 | def delay_netupdown(upness, reason): |
417 | global _delay | |
418 | cancel_delay() | |
419 | def _func(): | |
420 | global _delay | |
421 | if T._debug: print '# delayed %s: %s' % (upness, reason) | |
422 | _delay = None | |
423 | netupdown(upness, reason) | |
424 | return False | |
425 | if T._debug: print '# delaying %s: %s' % (upness, reason) | |
426 | _delay = G.timeout_add(2000, _func) | |
427 | ||
2ec90437 MW |
428 | def kickpeers(): |
429 | while True: | |
430 | upness, reason = _kick.get() | |
2d4998c4 MW |
431 | if T._debug: print '# kickpeers %s: %s' % (upness, reason) |
432 | select = [] | |
f5393555 | 433 | cancel_delay() |
2ec90437 MW |
434 | |
435 | ## Make sure the configuration file is up-to-date. Don't worry if we | |
436 | ## can't do anything useful. | |
437 | try: | |
438 | CF.check() | |
439 | except Exception, exc: | |
440 | SM.warn('conntrack', 'config-file-error', | |
441 | exc.__class__.__name__, str(exc)) | |
442 | ||
443 | ## Find the current list of peers. | |
444 | peers = SM.list() | |
445 | ||
e152ccf2 MW |
446 | ## Work out the primary IP addresses. |
447 | locals = {} | |
2ec90437 | 448 | if upness: |
e152ccf2 MW |
449 | for af, remote in CF.testaddrs.iteritems(): |
450 | local = localaddr(remote) | |
451 | if local is not None: locals[af] = local | |
452 | if not locals: upness = False | |
2d4998c4 | 453 | if not T._debug: pass |
e152ccf2 MW |
454 | elif not locals: print '# offline' |
455 | else: | |
456 | for local in locals.itervalues(): | |
457 | print '# local %s address = %s' % (local.AFNAME, local) | |
2ec90437 MW |
458 | |
459 | ## Now decide what to do. | |
460 | changes = [] | |
f8d6fc7b | 461 | for g, pp in CF.groups.iteritems(): |
2d4998c4 | 462 | if T._debug: print '# check group %s' % g |
2ec90437 MW |
463 | |
464 | ## Find out which peer in the group ought to be active. | |
31d7aa8d | 465 | statemap = {} |
b10a8c3d | 466 | want = None |
fa59a04b | 467 | matchp = False |
a59afe07 | 468 | for t, p, nn in pp: |
e152ccf2 MW |
469 | af = nn[0].AF |
470 | if p is None or not upness: ip = locals.get(af) | |
fa59a04b | 471 | else: ip = localaddr(p) |
2d4998c4 | 472 | if T._debug: |
a59afe07 MW |
473 | info = 'peer = %s; target = %s; nets = %s; local = %s' % ( |
474 | t, p or '(default)', ', '.join(map(str, nn)), straddr(ip)) | |
fa59a04b | 475 | if upness and not matchp and \ |
a59afe07 | 476 | ip is not None and any(ip.withinp(n) for n in nn): |
2d4998c4 | 477 | if T._debug: print '# %s: SELECTED' % info |
31d7aa8d | 478 | statemap[t] = 'up' |
2d4998c4 | 479 | select.append('%s=%s' % (g, t)) |
fa59a04b MW |
480 | if t == 'down' or t.startswith('down/'): want = None |
481 | else: want = t | |
482 | matchp = True | |
b10a8c3d | 483 | else: |
31d7aa8d | 484 | statemap[t] = 'down' |
2d4998c4 | 485 | if T._debug: print '# %s: skipped' % info |
2ec90437 MW |
486 | |
487 | ## Shut down the wrong ones. | |
488 | found = False | |
31d7aa8d | 489 | if T._debug: print '# peer-map = %r' % statemap |
2ec90437 | 490 | for p in peers: |
31d7aa8d | 491 | what = statemap.get(p, 'leave') |
b10a8c3d | 492 | if what == 'up': |
2ec90437 | 493 | found = True |
2d4998c4 | 494 | if T._debug: print '# peer %s: already up' % p |
b10a8c3d | 495 | elif what == 'down': |
cf2e4ea6 MW |
496 | def _(p = p): |
497 | try: | |
498 | SM.kill(p) | |
499 | except T.TripeError, exc: | |
500 | if exc.args[0] == 'unknown-peer': | |
501 | ## Inherently racy; don't worry about this. | |
502 | pass | |
503 | else: | |
504 | raise | |
2d4998c4 | 505 | if T._debug: print '# peer %s: bring down' % p |
cf2e4ea6 | 506 | changes.append(_) |
2ec90437 MW |
507 | |
508 | ## Start the right one if necessary. | |
7b7e3c74 | 509 | if want is not None and not found: |
cf2e4ea6 MW |
510 | def _(want = want): |
511 | try: | |
8d1d183e | 512 | list(SM.svcsubmit('connect', 'active', want)) |
cf2e4ea6 MW |
513 | except T.TripeError, exc: |
514 | SM.warn('conntrack', 'connect-failed', want, *exc.args) | |
2d4998c4 | 515 | if T._debug: print '# peer %s: bring up' % want |
cf2e4ea6 | 516 | changes.append(_) |
2ec90437 MW |
517 | |
518 | ## Commit the changes. | |
519 | if changes: | |
2d4998c4 | 520 | SM.notify('conntrack', upness and 'up' or 'down', *select + reason) |
2ec90437 MW |
521 | for c in changes: c() |
522 | ||
2ec90437 MW |
523 | ###-------------------------------------------------------------------------- |
524 | ### NetworkManager monitor. | |
525 | ||
498d9f42 MW |
526 | DBPROPS_IFACE = 'org.freedesktop.DBus.Properties' |
527 | ||
2ec90437 MW |
528 | NM_NAME = 'org.freedesktop.NetworkManager' |
529 | NM_PATH = '/org/freedesktop/NetworkManager' | |
530 | NM_IFACE = NM_NAME | |
531 | NMCA_IFACE = NM_NAME + '.Connection.Active' | |
532 | ||
2079efa1 MW |
533 | NM_STATE_CONNECTED = 3 #obsolete |
534 | NM_STATE_CONNECTED_LOCAL = 50 | |
535 | NM_STATE_CONNECTED_SITE = 60 | |
536 | NM_STATE_CONNECTED_GLOBAL = 70 | |
537 | NM_CONNSTATES = set([NM_STATE_CONNECTED, | |
538 | NM_STATE_CONNECTED_LOCAL, | |
539 | NM_STATE_CONNECTED_SITE, | |
540 | NM_STATE_CONNECTED_GLOBAL]) | |
2ec90437 MW |
541 | |
542 | class NetworkManagerMonitor (object): | |
543 | """ | |
544 | Watch NetworkManager signals for changes in network state. | |
545 | """ | |
546 | ||
547 | ## Strategy. There are two kinds of interesting state transitions for us. | |
548 | ## The first one is the global are-we-connected state, which we'll use to | |
549 | ## toggle network upness on a global level. The second is which connection | |
550 | ## has the default route, which we'll use to tweak which peer in the peer | |
551 | ## group is active. The former is most easily tracked using the signal | |
552 | ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track | |
553 | ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and | |
554 | ## look for when a new connection gains the default route. | |
555 | ||
556 | def attach(me, bus): | |
557 | try: | |
558 | nm = bus.get_object(NM_NAME, NM_PATH) | |
498d9f42 | 559 | state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE) |
2079efa1 | 560 | if state in NM_CONNSTATES: |
2ec90437 MW |
561 | netupdown(True, ['nm', 'initially-connected']) |
562 | else: | |
563 | netupdown(False, ['nm', 'initially-disconnected']) | |
bd9bd714 MW |
564 | except D.DBusException, e: |
565 | if T._debug: print '# exception attaching to network-manager: %s' % e | |
2079efa1 MW |
566 | bus.add_signal_receiver(me._nm_state, 'StateChanged', |
567 | NM_IFACE, NM_NAME, NM_PATH) | |
568 | bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged', | |
569 | NMCA_IFACE, NM_NAME, None) | |
2ec90437 MW |
570 | |
571 | def _nm_state(me, state): | |
2079efa1 | 572 | if state in NM_CONNSTATES: |
f5393555 | 573 | delay_netupdown(True, ['nm', 'connected']) |
2ec90437 | 574 | else: |
f5393555 | 575 | delay_netupdown(False, ['nm', 'disconnected']) |
2ec90437 MW |
576 | |
577 | def _nm_connchange(me, props): | |
f5393555 MW |
578 | if props.get('Default', False) or props.get('Default6', False): |
579 | delay_netupdown(True, ['nm', 'default-connection-change']) | |
2ec90437 | 580 | |
a95eb44a MW |
581 | ##-------------------------------------------------------------------------- |
582 | ### Connman monitor. | |
583 | ||
584 | CM_NAME = 'net.connman' | |
585 | CM_PATH = '/' | |
586 | CM_IFACE = 'net.connman.Manager' | |
587 | ||
588 | class ConnManMonitor (object): | |
589 | """ | |
590 | Watch ConnMan signls for changes in network state. | |
591 | """ | |
592 | ||
593 | ## Strategy. Everything seems to be usefully encoded in the `State' | |
594 | ## property. If it's `offline', `idle' or `ready' then we don't expect a | |
595 | ## network connection. During handover from one network to another, the | |
596 | ## property passes through `ready' to `online'. | |
597 | ||
598 | def attach(me, bus): | |
599 | try: | |
600 | cm = bus.get_object(CM_NAME, CM_PATH) | |
601 | props = cm.GetProperties(dbus_interface = CM_IFACE) | |
602 | state = props['State'] | |
603 | netupdown(state == 'online', ['connman', 'initially-%s' % state]) | |
bd9bd714 MW |
604 | except D.DBusException, e: |
605 | if T._debug: print '# exception attaching to connman: %s' % e | |
a95eb44a MW |
606 | bus.add_signal_receiver(me._cm_state, 'PropertyChanged', |
607 | CM_IFACE, CM_NAME, CM_PATH) | |
608 | ||
609 | def _cm_state(me, prop, value): | |
610 | if prop != 'State': return | |
f5393555 | 611 | delay_netupdown(value == 'online', ['connman', value]) |
a95eb44a | 612 | |
2ec90437 MW |
613 | ###-------------------------------------------------------------------------- |
614 | ### Maemo monitor. | |
615 | ||
616 | ICD_NAME = 'com.nokia.icd' | |
617 | ICD_PATH = '/com/nokia/icd' | |
618 | ICD_IFACE = ICD_NAME | |
619 | ||
620 | class MaemoICdMonitor (object): | |
621 | """ | |
622 | Watch ICd signals for changes in network state. | |
623 | """ | |
624 | ||
625 | ## Strategy. ICd only handles one connection at a time in steady state, | |
626 | ## though when switching between connections, it tries to bring the new one | |
627 | ## up before shutting down the old one. This makes life a bit easier than | |
628 | ## it is with NetworkManager. On the other hand, the notifications are | |
629 | ## relative to particular connections only, and the indicator that the old | |
630 | ## connection is down (`IDLE') comes /after/ the new one comes up | |
631 | ## (`CONNECTED'), so we have to remember which one is active. | |
632 | ||
633 | def attach(me, bus): | |
634 | try: | |
635 | icd = bus.get_object(ICD_NAME, ICD_PATH) | |
636 | try: | |
637 | iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0] | |
638 | me._iap = iap | |
639 | netupdown(True, ['icd', 'initially-connected', iap]) | |
640 | except D.DBusException: | |
641 | me._iap = None | |
642 | netupdown(False, ['icd', 'initially-disconnected']) | |
bd9bd714 MW |
643 | except D.DBusException, e: |
644 | if T._debug: print '# exception attaching to icd: %s' % e | |
2ec90437 MW |
645 | me._iap = None |
646 | bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE, | |
647 | ICD_NAME, ICD_PATH) | |
648 | ||
649 | def _icd_state(me, iap, ty, state, hunoz): | |
650 | if state == 'CONNECTED': | |
651 | me._iap = iap | |
f5393555 | 652 | delay_netupdown(True, ['icd', 'connected', iap]) |
2ec90437 MW |
653 | elif state == 'IDLE' and iap == me._iap: |
654 | me._iap = None | |
f5393555 | 655 | delay_netupdown(False, ['icd', 'idle']) |
2ec90437 MW |
656 | |
657 | ###-------------------------------------------------------------------------- | |
658 | ### D-Bus connection tracking. | |
659 | ||
660 | class DBusMonitor (object): | |
661 | """ | |
662 | Maintains a connection to the system D-Bus, and watches for signals. | |
663 | ||
664 | If the connection is initially down, or drops for some reason, we retry | |
665 | periodically (every five seconds at the moment). If the connection | |
666 | resurfaces, we reattach the monitors. | |
667 | """ | |
668 | ||
669 | def __init__(me): | |
670 | """ | |
671 | Initialise the object and try to establish a connection to the bus. | |
672 | """ | |
673 | me._mons = [] | |
674 | me._loop = D.mainloop.glib.DBusGMainLoop() | |
7bfa1e06 | 675 | me._state = 'startup' |
2ec90437 MW |
676 | me._reconnect() |
677 | ||
678 | def addmon(me, mon): | |
679 | """ | |
680 | Add a monitor object to watch for signals. | |
681 | ||
682 | MON.attach(BUS) is called, with BUS being the connection to the system | |
683 | bus. MON should query its service's current status and watch for | |
684 | relevant signals. | |
685 | """ | |
686 | me._mons.append(mon) | |
687 | if me._bus is not None: | |
688 | mon.attach(me._bus) | |
689 | ||
16650038 | 690 | def _reconnect(me, hunoz = None): |
2ec90437 MW |
691 | """ |
692 | Start connecting to the bus. | |
693 | ||
694 | If we fail the first time, retry periodically. | |
695 | """ | |
7bfa1e06 MW |
696 | if me._state == 'startup': |
697 | T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup') | |
698 | elif me._state == 'connected': | |
699 | T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost') | |
700 | else: | |
701 | T.aside(SM.notify, 'conntrack', 'dbus-connection', | |
702 | 'state=%s' % me._state) | |
703 | me._state == 'reconnecting' | |
2ec90437 MW |
704 | me._bus = None |
705 | if me._try_connect(): | |
706 | G.timeout_add_seconds(5, me._try_connect) | |
707 | ||
708 | def _try_connect(me): | |
709 | """ | |
710 | Actually make a connection attempt. | |
711 | ||
712 | If we succeed, attach the monitors. | |
713 | """ | |
714 | try: | |
7bfa1e06 MW |
715 | addr = OS.getenv('TRIPE_CONNTRACK_BUS') |
716 | if addr == 'SESSION': | |
717 | bus = D.SessionBus(mainloop = me._loop, private = True) | |
718 | elif addr is not None: | |
719 | bus = D.bus.BusConnection(addr, mainloop = me._loop) | |
720 | else: | |
721 | bus = D.SystemBus(mainloop = me._loop, private = True) | |
722 | for m in me._mons: | |
723 | m.attach(bus) | |
724 | except D.DBusException, e: | |
2ec90437 MW |
725 | return True |
726 | me._bus = bus | |
7bfa1e06 | 727 | me._state = 'connected' |
2ec90437 | 728 | bus.call_on_disconnection(me._reconnect) |
7bfa1e06 | 729 | T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected') |
2ec90437 MW |
730 | return False |
731 | ||
732 | ###-------------------------------------------------------------------------- | |
733 | ### TrIPE service. | |
734 | ||
735 | class GIOWatcher (object): | |
736 | """ | |
737 | Monitor I/O events using glib. | |
738 | """ | |
739 | def __init__(me, conn, mc = G.main_context_default()): | |
740 | me._conn = conn | |
741 | me._watch = None | |
742 | me._mc = mc | |
743 | def connected(me, sock): | |
744 | me._watch = G.io_add_watch(sock, G.IO_IN, | |
745 | lambda *hunoz: me._conn.receive()) | |
746 | def disconnected(me): | |
747 | G.source_remove(me._watch) | |
748 | me._watch = None | |
749 | def iterate(me): | |
750 | me._mc.iteration(True) | |
751 | ||
752 | SM.iowatch = GIOWatcher(SM) | |
753 | ||
754 | def init(): | |
755 | """ | |
756 | Service initialization. | |
757 | ||
758 | Add the D-Bus monitor here, because we might send commands off immediately, | |
759 | and we want to make sure the server connection is up. | |
760 | """ | |
29807d89 | 761 | global DBM |
22b47552 | 762 | T.Coroutine(kickpeers, name = 'kickpeers').switch() |
29807d89 MW |
763 | DBM = DBusMonitor() |
764 | DBM.addmon(NetworkManagerMonitor()) | |
a95eb44a | 765 | DBM.addmon(ConnManMonitor()) |
29807d89 | 766 | DBM.addmon(MaemoICdMonitor()) |
f5393555 MW |
767 | G.timeout_add_seconds(30, lambda: (_delay is not None or |
768 | netupdown(True, ['interval-timer']) or | |
769 | True)) | |
2ec90437 MW |
770 | |
771 | def parse_options(): | |
772 | """ | |
773 | Parse the command-line options. | |
774 | ||
775 | Automatically changes directory to the requested configdir, and turns on | |
776 | debugging. Returns the options object. | |
777 | """ | |
778 | op = OptionParser(usage = '%prog [-a FILE] [-d DIR]', | |
779 | version = '%%prog %s' % VERSION) | |
780 | ||
781 | op.add_option('-a', '--admin-socket', | |
782 | metavar = 'FILE', dest = 'tripesock', default = T.tripesock, | |
783 | help = 'Select socket to connect to [default %default]') | |
784 | op.add_option('-d', '--directory', | |
785 | metavar = 'DIR', dest = 'dir', default = T.configdir, | |
786 | help = 'Select current diretory [default %default]') | |
787 | op.add_option('-c', '--config', | |
788 | metavar = 'FILE', dest = 'conf', default = 'conntrack.conf', | |
789 | help = 'Select configuration [default %default]') | |
790 | op.add_option('--daemon', dest = 'daemon', | |
791 | default = False, action = 'store_true', | |
792 | help = 'Become a daemon after successful initialization') | |
793 | op.add_option('--debug', dest = 'debug', | |
794 | default = False, action = 'store_true', | |
795 | help = 'Emit debugging trace information') | |
796 | op.add_option('--startup', dest = 'startup', | |
797 | default = False, action = 'store_true', | |
798 | help = 'Being called as part of the server startup') | |
799 | ||
800 | opts, args = op.parse_args() | |
801 | if args: op.error('no arguments permitted') | |
802 | OS.chdir(opts.dir) | |
803 | T._debug = opts.debug | |
804 | return opts | |
805 | ||
806 | ## Service table, for running manually. | |
807 | def cmd_updown(upness): | |
808 | return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args)) | |
809 | service_info = [('conntrack', VERSION, { | |
810 | 'up': (0, None, '', cmd_updown(True)), | |
2d4998c4 MW |
811 | 'down': (0, None, '', cmd_updown(False)), |
812 | 'show-config': (0, 0, '', cmd_showconfig), | |
813 | 'show-groups': (0, 0, '', cmd_showgroups), | |
814 | 'show-group': (1, 1, 'GROUP', cmd_showgroup) | |
2ec90437 MW |
815 | })] |
816 | ||
817 | if __name__ == '__main__': | |
818 | opts = parse_options() | |
819 | CF = Config(opts.conf) | |
820 | T.runservices(opts.tripesock, service_info, | |
821 | init = init, daemon = opts.daemon) | |
822 | ||
823 | ###----- That's all, folks -------------------------------------------------- |