chiark / gitweb /
Merge branches 'mdw/knock' and 'mdw/ipv6' into bleeding
[tripe] / svc / conntrack.in
CommitLineData
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
26VERSION = '@VERSION@'
27
28###--------------------------------------------------------------------------
29### External dependencies.
30
31from ConfigParser import RawConfigParser
32from optparse import OptionParser
33import os as OS
34import sys as SYS
35import socket as S
36import mLib as M
37import tripe as T
38import dbus as D
f8204e50 39import re as RX
2ec90437
MW
40for i in ['mainloop', 'mainloop.glib']:
41 __import__('dbus.%s' % i)
a69f4417
MW
42try: from gi.repository import GLib as G
43except ImportError: import gobject as G
2ec90437 44from struct import pack, unpack
c21d936a 45from cStringIO import StringIO
2ec90437
MW
46
47SM = T.svcmgr
48##__import__('rmcr').__debug = True
49
50###--------------------------------------------------------------------------
51### Utilities.
52
53class struct (object):
54 """A simple container object."""
55 def __init__(me, **kw):
56 me.__dict__.update(kw)
57
c21d936a
MW
58def loadb(s):
59 n = 0
60 for ch in s: n = 256*n + ord(ch)
61 return n
62
63def 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 76class 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
124class 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
146class 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 181def 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
185def 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 190def 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 200TESTADDRS = [InetAddress('1.2.3.4'), Inet6Address('2001::1')]
6aa21132 201
f8204e50
MW
202CONFSYNTAX = [
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
207class 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
215class 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.
361CF = None
362
2d4998c4 363def 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 368def cmd_showgroups():
f8d6fc7b
MW
369 for g in sorted(CF.groups.iterkeys()):
370 T.svcinfo(g)
2d4998c4 371def 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
382def 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
400def 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
407def 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
416def 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
428def 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
526DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
527
2ec90437
MW
528NM_NAME = 'org.freedesktop.NetworkManager'
529NM_PATH = '/org/freedesktop/NetworkManager'
530NM_IFACE = NM_NAME
531NMCA_IFACE = NM_NAME + '.Connection.Active'
532
2079efa1
MW
533NM_STATE_CONNECTED = 3 #obsolete
534NM_STATE_CONNECTED_LOCAL = 50
535NM_STATE_CONNECTED_SITE = 60
536NM_STATE_CONNECTED_GLOBAL = 70
537NM_CONNSTATES = set([NM_STATE_CONNECTED,
538 NM_STATE_CONNECTED_LOCAL,
539 NM_STATE_CONNECTED_SITE,
540 NM_STATE_CONNECTED_GLOBAL])
2ec90437
MW
541
542class 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
584CM_NAME = 'net.connman'
585CM_PATH = '/'
586CM_IFACE = 'net.connman.Manager'
587
588class 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
616ICD_NAME = 'com.nokia.icd'
617ICD_PATH = '/com/nokia/icd'
618ICD_IFACE = ICD_NAME
619
620class 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
660class 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
735class 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
752SM.iowatch = GIOWatcher(SM)
753
754def 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
771def 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.
807def cmd_updown(upness):
808 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
809service_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
817if __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 --------------------------------------------------