4 ### Service for automatically tracking network connection status
6 ### (c) 2010 Straylight/Edgeware
9 ###----- Licensing notice ---------------------------------------------------
11 ### This file is part of Trivial IP Encryption (TrIPE).
13 ### TrIPE is free software; you can redistribute it and/or modify
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ### GNU General Public License for more details.
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ###--------------------------------------------------------------------------
30 ### External dependencies.
32 from ConfigParser import RawConfigParser
33 from optparse import OptionParser
40 for i in ['mainloop', 'mainloop.glib']:
41 __import__('dbus.%s' % i)
42 try: from gi.repository import GLib as G
43 except ImportError: import gobject as G
44 from struct import pack, unpack
47 ##__import__('rmcr').__debug = True
49 ###--------------------------------------------------------------------------
52 class struct (object):
53 """A simple container object."""
54 def __init__(me, **kw):
55 me.__dict__.update(kw)
57 def toposort(cmp, things):
59 Generate the THINGS in an order consistent with a given partial order.
61 The function CMP(X, Y) should return true if X must precede Y, and false if
62 it doesn't care. If X and Y are equal then it should return false.
64 The THINGS may be any finite iterable; it is converted to a list
68 ## Make sure we can index the THINGS, and prepare an ordering table.
69 ## What's going on? The THINGS might not have a helpful equality
70 ## predicate, so it's easier to work with indices. The ordering table will
71 ## remember which THINGS (by index) are considered greater than other
75 order = [{} for i in xrange(n)]
76 rorder = [{} for i in xrange(n)]
79 if i != j and cmp(things[i], things[j]):
83 ## Now we can do the sort.
88 if order[i] is not None:
90 if len(order[i]) == 0:
98 ###--------------------------------------------------------------------------
99 ### Parse the configuration file.
101 ## Hmm. Should I try to integrate this with the peers database? It's not a
102 ## good fit; it'd need special hacks in tripe-newpeers. And the use case for
103 ## this service are largely going to be satellite notes, I don't think
104 ## scalability's going to be a problem.
106 class Config (object):
108 Represents a configuration file.
110 The most interesting thing is probably the `groups' slot, which stores a
111 list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
112 list of (TAG, PEER, ADDR, MASK) triples. The implication is that there
113 should be precisely one peer with a name matching NAME-*, and that it
114 should be NAME-TAG, where (TAG, PEER, ADDR, MASK) is the first triple such
115 that the host's primary IP address (if PEER is None -- or the IP address it
116 would use for communicating with PEER) is within the network defined by
120 def __init__(me, file):
122 Construct a new Config object, reading the given FILE.
125 me._fwatch = M.FWatch(file)
130 See whether the configuration file has been updated.
132 if me._fwatch.update():
137 Internal function to update the configuration from the underlying file.
140 ## Read the configuration. We have no need of the fancy substitutions,
141 ## so turn them all off.
142 cp = RawConfigParser()
144 if T._debug: print '# reread config'
146 ## Save the test address. Make sure it's vaguely sensible. The default
147 ## is probably good for most cases, in fact, since that address isn't
148 ## actually in use. Note that we never send packets to the test address;
149 ## we just use it to discover routing information.
150 if cp.has_option('DEFAULT', 'test-addr'):
151 testaddr = cp.get('DEFAULT', 'test-addr')
152 S.inet_aton(testaddr)
156 ## Scan the configuration file and build the groups structure.
158 for sec in cp.sections():
160 for tag in cp.options(sec):
161 spec = cp.get(sec, tag).split()
163 ## Parse the entry into peer and network.
170 ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
171 ## and MASK is either a dotted-quad or a single integer N indicating
172 ## a mask with N leading ones followed by trailing zeroes.
173 slash = net.index('/')
174 addr, = unpack('>L', S.inet_aton(net[:slash]))
175 if net.find('.', slash + 1) >= 0:
176 mask, = unpack('>L', S.inet_aton(net[:slash]))
178 n = int(net[slash + 1:], 10)
179 mask = (1 << 32) - (1 << 32 - n)
180 pats.append((tag, peer, addr & mask, mask))
182 ## Annoyingly, RawConfigParser doesn't preserve the order of options.
183 ## In order to make things vaguely sane, we topologically sort the
184 ## patterns so that more specific patterns are checked first.
185 pats = list(toposort(lambda (t, p, a, m), (tt, pp, aa, mm): \
187 (p == pp and m == (m | mm) and aa == (a & mm)),
189 groups.append((sec, pats))
192 me.testaddr = testaddr
195 ### This will be a configuration file.
198 def straddr(a): return a is None and '#<none>' or S.inet_ntoa(pack('>L', a))
201 if m == 0xffffffff ^ ((1 << (32 - i)) - 1): return i
204 def cmd_showconfig():
205 T.svcinfo('test-addr=%s' % CF.testaddr)
206 def cmd_showgroups():
207 for sec, pats in CF.groups:
209 def cmd_showgroup(g):
210 for s, p in CF.groups:
215 raise T.TripeJobError, 'unknown-group', g
216 for t, p, a, m in pats:
218 'target', p or '(default)',
219 'net', '%s/%s' % (straddr(a), strmask(m)))
221 ###--------------------------------------------------------------------------
222 ### Responding to a network up/down event.
226 Return the local IP address used for talking to PEER.
228 sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
231 sk.connect((peer, 1))
232 addr, _ = sk.getsockname()
233 addr, = unpack('>L', S.inet_aton(addr))
243 upness, reason = _kick.get()
244 if T._debug: print '# kickpeers %s: %s' % (upness, reason)
247 ## Make sure the configuration file is up-to-date. Don't worry if we
248 ## can't do anything useful.
251 except Exception, exc:
252 SM.warn('conntrack', 'config-file-error',
253 exc.__class__.__name__, str(exc))
255 ## Find the current list of peers.
258 ## Work out the primary IP address.
260 addr = localaddr(CF.testaddr)
265 if not T._debug: pass
266 elif addr: print '# local address = %s' % straddr(addr)
267 else: print '# offline'
269 ## Now decide what to do.
271 for g, pp in CF.groups:
272 if T._debug: print '# check group %s' % g
274 ## Find out which peer in the group ought to be active.
278 for t, p, a, m in pp:
279 if p is None or not upness:
284 info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
285 t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
286 if upness and ip is None and \
287 ipq is not None and (ipq & m) == a:
288 if T._debug: print '# %s: SELECTED' % info
290 select.append('%s=%s' % (g, t))
291 if t == 'down' or t.startswith('down/'):
298 if T._debug: print '# %s: skipped' % info
300 ## Shut down the wrong ones.
302 if T._debug: print '# peer-map = %r' % map
304 what = map.get(p, 'leave')
307 if T._debug: print '# peer %s: already up' % p
312 except T.TripeError, exc:
313 if exc.args[0] == 'unknown-peer':
314 ## Inherently racy; don't worry about this.
318 if T._debug: print '# peer %s: bring down' % p
321 ## Start the right one if necessary.
322 if want is not None and not found:
325 SM.svcsubmit('connect', 'active', want)
326 except T.TripeError, exc:
327 SM.warn('conntrack', 'connect-failed', want, *exc.args)
328 if T._debug: print '# peer %s: bring up' % want
331 ## Commit the changes.
333 SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
334 for c in changes: c()
336 def netupdown(upness, reason):
338 Add or kill peers according to whether the network is up or down.
340 UPNESS is true if the network is up, or false if it's down.
343 _kick.put((upness, reason))
345 ###--------------------------------------------------------------------------
346 ### NetworkManager monitor.
348 NM_NAME = 'org.freedesktop.NetworkManager'
349 NM_PATH = '/org/freedesktop/NetworkManager'
351 NMCA_IFACE = NM_NAME + '.Connection.Active'
353 NM_STATE_CONNECTED = 3 #obsolete
354 NM_STATE_CONNECTED_LOCAL = 50
355 NM_STATE_CONNECTED_SITE = 60
356 NM_STATE_CONNECTED_GLOBAL = 70
357 NM_CONNSTATES = set([NM_STATE_CONNECTED,
358 NM_STATE_CONNECTED_LOCAL,
359 NM_STATE_CONNECTED_SITE,
360 NM_STATE_CONNECTED_GLOBAL])
362 class NetworkManagerMonitor (object):
364 Watch NetworkManager signals for changes in network state.
367 ## Strategy. There are two kinds of interesting state transitions for us.
368 ## The first one is the global are-we-connected state, which we'll use to
369 ## toggle network upness on a global level. The second is which connection
370 ## has the default route, which we'll use to tweak which peer in the peer
371 ## group is active. The former is most easily tracked using the signal
372 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
373 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
374 ## look for when a new connection gains the default route.
378 nm = bus.get_object(NM_NAME, NM_PATH)
379 state = nm.Get(NM_IFACE, 'State')
380 if state in NM_CONNSTATES:
381 netupdown(True, ['nm', 'initially-connected'])
383 netupdown(False, ['nm', 'initially-disconnected'])
384 except D.DBusException:
386 bus.add_signal_receiver(me._nm_state, 'StateChanged',
387 NM_IFACE, NM_NAME, NM_PATH)
388 bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
389 NMCA_IFACE, NM_NAME, None)
391 def _nm_state(me, state):
392 if state in NM_CONNSTATES:
393 netupdown(True, ['nm', 'connected'])
395 netupdown(False, ['nm', 'disconnected'])
397 def _nm_connchange(me, props):
398 if props.get('Default', False):
399 netupdown(True, ['nm', 'default-connection-change'])
401 ###--------------------------------------------------------------------------
404 ICD_NAME = 'com.nokia.icd'
405 ICD_PATH = '/com/nokia/icd'
408 class MaemoICdMonitor (object):
410 Watch ICd signals for changes in network state.
413 ## Strategy. ICd only handles one connection at a time in steady state,
414 ## though when switching between connections, it tries to bring the new one
415 ## up before shutting down the old one. This makes life a bit easier than
416 ## it is with NetworkManager. On the other hand, the notifications are
417 ## relative to particular connections only, and the indicator that the old
418 ## connection is down (`IDLE') comes /after/ the new one comes up
419 ## (`CONNECTED'), so we have to remember which one is active.
423 icd = bus.get_object(ICD_NAME, ICD_PATH)
425 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
427 netupdown(True, ['icd', 'initially-connected', iap])
428 except D.DBusException:
430 netupdown(False, ['icd', 'initially-disconnected'])
431 except D.DBusException:
433 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
436 def _icd_state(me, iap, ty, state, hunoz):
437 if state == 'CONNECTED':
439 netupdown(True, ['icd', 'connected', iap])
440 elif state == 'IDLE' and iap == me._iap:
442 netupdown(False, ['icd', 'idle'])
444 ###--------------------------------------------------------------------------
445 ### D-Bus connection tracking.
447 class DBusMonitor (object):
449 Maintains a connection to the system D-Bus, and watches for signals.
451 If the connection is initially down, or drops for some reason, we retry
452 periodically (every five seconds at the moment). If the connection
453 resurfaces, we reattach the monitors.
458 Initialise the object and try to establish a connection to the bus.
461 me._loop = D.mainloop.glib.DBusGMainLoop()
462 me._state = 'startup'
467 Add a monitor object to watch for signals.
469 MON.attach(BUS) is called, with BUS being the connection to the system
470 bus. MON should query its service's current status and watch for
474 if me._bus is not None:
477 def _reconnect(me, hunoz = None):
479 Start connecting to the bus.
481 If we fail the first time, retry periodically.
483 if me._state == 'startup':
484 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
485 elif me._state == 'connected':
486 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
488 T.aside(SM.notify, 'conntrack', 'dbus-connection',
489 'state=%s' % me._state)
490 me._state == 'reconnecting'
492 if me._try_connect():
493 G.timeout_add_seconds(5, me._try_connect)
495 def _try_connect(me):
497 Actually make a connection attempt.
499 If we succeed, attach the monitors.
502 addr = OS.getenv('TRIPE_CONNTRACK_BUS')
503 if addr == 'SESSION':
504 bus = D.SessionBus(mainloop = me._loop, private = True)
505 elif addr is not None:
506 bus = D.bus.BusConnection(addr, mainloop = me._loop)
508 bus = D.SystemBus(mainloop = me._loop, private = True)
511 except D.DBusException, e:
514 me._state = 'connected'
515 bus.call_on_disconnection(me._reconnect)
516 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
519 ###--------------------------------------------------------------------------
522 class GIOWatcher (object):
524 Monitor I/O events using glib.
526 def __init__(me, conn, mc = G.main_context_default()):
530 def connected(me, sock):
531 me._watch = G.io_add_watch(sock, G.IO_IN,
532 lambda *hunoz: me._conn.receive())
533 def disconnected(me):
534 G.source_remove(me._watch)
537 me._mc.iteration(True)
539 SM.iowatch = GIOWatcher(SM)
543 Service initialization.
545 Add the D-Bus monitor here, because we might send commands off immediately,
546 and we want to make sure the server connection is up.
549 T.Coroutine(kickpeers, name = 'kickpeers').switch()
551 DBM.addmon(NetworkManagerMonitor())
552 DBM.addmon(MaemoICdMonitor())
553 G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
558 Parse the command-line options.
560 Automatically changes directory to the requested configdir, and turns on
561 debugging. Returns the options object.
563 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
564 version = '%%prog %s' % VERSION)
566 op.add_option('-a', '--admin-socket',
567 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
568 help = 'Select socket to connect to [default %default]')
569 op.add_option('-d', '--directory',
570 metavar = 'DIR', dest = 'dir', default = T.configdir,
571 help = 'Select current diretory [default %default]')
572 op.add_option('-c', '--config',
573 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
574 help = 'Select configuration [default %default]')
575 op.add_option('--daemon', dest = 'daemon',
576 default = False, action = 'store_true',
577 help = 'Become a daemon after successful initialization')
578 op.add_option('--debug', dest = 'debug',
579 default = False, action = 'store_true',
580 help = 'Emit debugging trace information')
581 op.add_option('--startup', dest = 'startup',
582 default = False, action = 'store_true',
583 help = 'Being called as part of the server startup')
585 opts, args = op.parse_args()
586 if args: op.error('no arguments permitted')
588 T._debug = opts.debug
591 ## Service table, for running manually.
592 def cmd_updown(upness):
593 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
594 service_info = [('conntrack', VERSION, {
595 'up': (0, None, '', cmd_updown(True)),
596 'down': (0, None, '', cmd_updown(False)),
597 'show-config': (0, 0, '', cmd_showconfig),
598 'show-groups': (0, 0, '', cmd_showgroups),
599 'show-group': (1, 1, 'GROUP', cmd_showgroup)
602 if __name__ == '__main__':
603 opts = parse_options()
604 CF = Config(opts.conf)
605 T.runservices(opts.tripesock, service_info,
606 init = init, daemon = opts.daemon)
608 ###----- That's all, folks --------------------------------------------------