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