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 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.
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
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
28 ###--------------------------------------------------------------------------
29 ### External dependencies.
31 from ConfigParser import RawConfigParser
32 from optparse import OptionParser
39 for i in ['mainloop', 'mainloop.glib']:
40 __import__('dbus.%s' % i)
41 try: from gi.repository import GLib as G
42 except ImportError: import gobject as G
43 from struct import pack, unpack
46 ##__import__('rmcr').__debug = True
48 ###--------------------------------------------------------------------------
51 class struct (object):
52 """A simple container object."""
53 def __init__(me, **kw):
54 me.__dict__.update(kw)
56 def toposort(cmp, things):
58 Generate the THINGS in an order consistent with a given partial order.
60 The function CMP(X, Y) should return true if X must precede Y, and false if
61 it doesn't care. If X and Y are equal then it should return false.
63 The THINGS may be any finite iterable; it is converted to a list
67 ## Make sure we can index the THINGS, and prepare an ordering table.
68 ## What's going on? The THINGS might not have a helpful equality
69 ## predicate, so it's easier to work with indices. The ordering table will
70 ## remember which THINGS (by index) are considered greater than other
74 order = [{} for i in xrange(n)]
75 rorder = [{} for i in xrange(n)]
78 if i != j and cmp(things[i], things[j]):
82 ## Now we can do the sort.
87 if order[i] is not None:
89 if len(order[i]) == 0:
97 ###--------------------------------------------------------------------------
98 ### Parse the configuration file.
100 ## Hmm. Should I try to integrate this with the peers database? It's not a
101 ## good fit; it'd need special hacks in tripe-newpeers. And the use case for
102 ## this service are largely going to be satellite notes, I don't think
103 ## scalability's going to be a problem.
105 class Config (object):
107 Represents a configuration file.
109 The most interesting thing is probably the `groups' slot, which stores a
110 list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
111 list of (TAG, PEER, ADDR, MASK) triples. The implication is that there
112 should be precisely one peer with a name matching NAME-*, and that it
113 should be NAME-TAG, where (TAG, PEER, ADDR, MASK) is the first triple such
114 that the host's primary IP address (if PEER is None -- or the IP address it
115 would use for communicating with PEER) is within the network defined by
119 def __init__(me, file):
121 Construct a new Config object, reading the given FILE.
124 me._fwatch = M.FWatch(file)
129 See whether the configuration file has been updated.
131 if me._fwatch.update():
136 Internal function to update the configuration from the underlying file.
139 ## Read the configuration. We have no need of the fancy substitutions,
140 ## so turn them all off.
141 cp = RawConfigParser()
143 if T._debug: print '# reread config'
145 ## Save the test address. Make sure it's vaguely sensible. The default
146 ## is probably good for most cases, in fact, since that address isn't
147 ## actually in use. Note that we never send packets to the test address;
148 ## we just use it to discover routing information.
149 if cp.has_option('DEFAULT', 'test-addr'):
150 testaddr = cp.get('DEFAULT', 'test-addr')
151 S.inet_aton(testaddr)
155 ## Scan the configuration file and build the groups structure.
157 for sec in cp.sections():
159 for tag in cp.options(sec):
160 spec = cp.get(sec, tag).split()
162 ## Parse the entry into peer and network.
169 ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
170 ## and MASK is either a dotted-quad or a single integer N indicating
171 ## a mask with N leading ones followed by trailing zeroes.
172 slash = net.index('/')
173 addr, = unpack('>L', S.inet_aton(net[:slash]))
174 if net.find('.', slash + 1) >= 0:
175 mask, = unpack('>L', S.inet_aton(net[:slash]))
177 n = int(net[slash + 1:], 10)
178 mask = (1 << 32) - (1 << 32 - n)
179 pats.append((tag, peer, addr & mask, mask))
181 ## Annoyingly, RawConfigParser doesn't preserve the order of options.
182 ## In order to make things vaguely sane, we topologically sort the
183 ## patterns so that more specific patterns are checked first.
184 pats = list(toposort(lambda (t, p, a, m), (tt, pp, aa, mm): \
186 (p == pp and m == (m | mm) and aa == (a & mm)),
188 groups.append((sec, pats))
191 me.testaddr = testaddr
194 ### This will be a configuration file.
197 def straddr(a): return a is None and '#<none>' or S.inet_ntoa(pack('>L', a))
200 if m == 0xffffffff ^ ((1 << (32 - i)) - 1): return i
203 def cmd_showconfig():
204 T.svcinfo('test-addr=%s' % CF.testaddr)
205 def cmd_showgroups():
206 for sec, pats in CF.groups:
208 def cmd_showgroup(g):
209 for s, p in CF.groups:
214 raise T.TripeJobError('unknown-group', g)
215 for t, p, a, m in pats:
217 'target', p or '(default)',
218 'net', '%s/%s' % (straddr(a), strmask(m)))
220 ###--------------------------------------------------------------------------
221 ### Responding to a network up/down event.
225 Return the local IP address used for talking to PEER.
227 sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
230 sk.connect((peer, 1))
231 addr, _ = sk.getsockname()
232 addr, = unpack('>L', S.inet_aton(addr))
241 def netupdown(upness, reason):
243 Add or kill peers according to whether the network is up or down.
245 UPNESS is true if the network is up, or false if it's down.
248 _kick.put((upness, reason))
252 upness, reason = _kick.get()
253 if T._debug: print '# kickpeers %s: %s' % (upness, reason)
256 ## Make sure the configuration file is up-to-date. Don't worry if we
257 ## can't do anything useful.
260 except Exception, exc:
261 SM.warn('conntrack', 'config-file-error',
262 exc.__class__.__name__, str(exc))
264 ## Find the current list of peers.
267 ## Work out the primary IP address.
269 addr = localaddr(CF.testaddr)
274 if not T._debug: pass
275 elif addr: print '# local address = %s' % straddr(addr)
276 else: print '# offline'
278 ## Now decide what to do.
280 for g, pp in CF.groups:
281 if T._debug: print '# check group %s' % g
283 ## Find out which peer in the group ought to be active.
287 for t, p, a, m in pp:
288 if p is None or not upness:
293 info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
294 t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
295 if upness and ip is None and \
296 ipq is not None and (ipq & m) == a:
297 if T._debug: print '# %s: SELECTED' % info
299 select.append('%s=%s' % (g, t))
300 if t == 'down' or t.startswith('down/'):
307 if T._debug: print '# %s: skipped' % info
309 ## Shut down the wrong ones.
311 if T._debug: print '# peer-map = %r' % map
313 what = map.get(p, 'leave')
316 if T._debug: print '# peer %s: already up' % p
321 except T.TripeError, exc:
322 if exc.args[0] == 'unknown-peer':
323 ## Inherently racy; don't worry about this.
327 if T._debug: print '# peer %s: bring down' % p
330 ## Start the right one if necessary.
331 if want is not None and not found:
334 list(SM.svcsubmit('connect', 'active', want))
335 except T.TripeError, exc:
336 SM.warn('conntrack', 'connect-failed', want, *exc.args)
337 if T._debug: print '# peer %s: bring up' % want
340 ## Commit the changes.
342 SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
343 for c in changes: c()
345 ###--------------------------------------------------------------------------
346 ### NetworkManager monitor.
348 DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
350 NM_NAME = 'org.freedesktop.NetworkManager'
351 NM_PATH = '/org/freedesktop/NetworkManager'
353 NMCA_IFACE = NM_NAME + '.Connection.Active'
355 NM_STATE_CONNECTED = 3 #obsolete
356 NM_STATE_CONNECTED_LOCAL = 50
357 NM_STATE_CONNECTED_SITE = 60
358 NM_STATE_CONNECTED_GLOBAL = 70
359 NM_CONNSTATES = set([NM_STATE_CONNECTED,
360 NM_STATE_CONNECTED_LOCAL,
361 NM_STATE_CONNECTED_SITE,
362 NM_STATE_CONNECTED_GLOBAL])
364 class NetworkManagerMonitor (object):
366 Watch NetworkManager signals for changes in network state.
369 ## Strategy. There are two kinds of interesting state transitions for us.
370 ## The first one is the global are-we-connected state, which we'll use to
371 ## toggle network upness on a global level. The second is which connection
372 ## has the default route, which we'll use to tweak which peer in the peer
373 ## group is active. The former is most easily tracked using the signal
374 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
375 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
376 ## look for when a new connection gains the default route.
380 nm = bus.get_object(NM_NAME, NM_PATH)
381 state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE)
382 if state in NM_CONNSTATES:
383 netupdown(True, ['nm', 'initially-connected'])
385 netupdown(False, ['nm', 'initially-disconnected'])
386 except D.DBusException, e:
387 if T._debug: print '# exception attaching to network-manager: %s' % e
388 bus.add_signal_receiver(me._nm_state, 'StateChanged',
389 NM_IFACE, NM_NAME, NM_PATH)
390 bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
391 NMCA_IFACE, NM_NAME, None)
393 def _nm_state(me, state):
394 if state in NM_CONNSTATES:
395 netupdown(True, ['nm', 'connected'])
397 netupdown(False, ['nm', 'disconnected'])
399 def _nm_connchange(me, props):
400 if props.get('Default', False):
401 netupdown(True, ['nm', 'default-connection-change'])
403 ##--------------------------------------------------------------------------
406 CM_NAME = 'net.connman'
408 CM_IFACE = 'net.connman.Manager'
410 class ConnManMonitor (object):
412 Watch ConnMan signls for changes in network state.
415 ## Strategy. Everything seems to be usefully encoded in the `State'
416 ## property. If it's `offline', `idle' or `ready' then we don't expect a
417 ## network connection. During handover from one network to another, the
418 ## property passes through `ready' to `online'.
422 cm = bus.get_object(CM_NAME, CM_PATH)
423 props = cm.GetProperties(dbus_interface = CM_IFACE)
424 state = props['State']
425 netupdown(state == 'online', ['connman', 'initially-%s' % state])
426 except D.DBusException, e:
427 if T._debug: print '# exception attaching to connman: %s' % e
428 bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
429 CM_IFACE, CM_NAME, CM_PATH)
431 def _cm_state(me, prop, value):
432 if prop != 'State': return
433 netupdown(value == 'online', ['connman', value])
435 ###--------------------------------------------------------------------------
438 ICD_NAME = 'com.nokia.icd'
439 ICD_PATH = '/com/nokia/icd'
442 class MaemoICdMonitor (object):
444 Watch ICd signals for changes in network state.
447 ## Strategy. ICd only handles one connection at a time in steady state,
448 ## though when switching between connections, it tries to bring the new one
449 ## up before shutting down the old one. This makes life a bit easier than
450 ## it is with NetworkManager. On the other hand, the notifications are
451 ## relative to particular connections only, and the indicator that the old
452 ## connection is down (`IDLE') comes /after/ the new one comes up
453 ## (`CONNECTED'), so we have to remember which one is active.
457 icd = bus.get_object(ICD_NAME, ICD_PATH)
459 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
461 netupdown(True, ['icd', 'initially-connected', iap])
462 except D.DBusException:
464 netupdown(False, ['icd', 'initially-disconnected'])
465 except D.DBusException, e:
466 if T._debug: print '# exception attaching to icd: %s' % e
468 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
471 def _icd_state(me, iap, ty, state, hunoz):
472 if state == 'CONNECTED':
474 netupdown(True, ['icd', 'connected', iap])
475 elif state == 'IDLE' and iap == me._iap:
477 netupdown(False, ['icd', 'idle'])
479 ###--------------------------------------------------------------------------
480 ### D-Bus connection tracking.
482 class DBusMonitor (object):
484 Maintains a connection to the system D-Bus, and watches for signals.
486 If the connection is initially down, or drops for some reason, we retry
487 periodically (every five seconds at the moment). If the connection
488 resurfaces, we reattach the monitors.
493 Initialise the object and try to establish a connection to the bus.
496 me._loop = D.mainloop.glib.DBusGMainLoop()
497 me._state = 'startup'
502 Add a monitor object to watch for signals.
504 MON.attach(BUS) is called, with BUS being the connection to the system
505 bus. MON should query its service's current status and watch for
509 if me._bus is not None:
512 def _reconnect(me, hunoz = None):
514 Start connecting to the bus.
516 If we fail the first time, retry periodically.
518 if me._state == 'startup':
519 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
520 elif me._state == 'connected':
521 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
523 T.aside(SM.notify, 'conntrack', 'dbus-connection',
524 'state=%s' % me._state)
525 me._state == 'reconnecting'
527 if me._try_connect():
528 G.timeout_add_seconds(5, me._try_connect)
530 def _try_connect(me):
532 Actually make a connection attempt.
534 If we succeed, attach the monitors.
537 addr = OS.getenv('TRIPE_CONNTRACK_BUS')
538 if addr == 'SESSION':
539 bus = D.SessionBus(mainloop = me._loop, private = True)
540 elif addr is not None:
541 bus = D.bus.BusConnection(addr, mainloop = me._loop)
543 bus = D.SystemBus(mainloop = me._loop, private = True)
546 except D.DBusException, e:
549 me._state = 'connected'
550 bus.call_on_disconnection(me._reconnect)
551 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
554 ###--------------------------------------------------------------------------
557 class GIOWatcher (object):
559 Monitor I/O events using glib.
561 def __init__(me, conn, mc = G.main_context_default()):
565 def connected(me, sock):
566 me._watch = G.io_add_watch(sock, G.IO_IN,
567 lambda *hunoz: me._conn.receive())
568 def disconnected(me):
569 G.source_remove(me._watch)
572 me._mc.iteration(True)
574 SM.iowatch = GIOWatcher(SM)
578 Service initialization.
580 Add the D-Bus monitor here, because we might send commands off immediately,
581 and we want to make sure the server connection is up.
584 T.Coroutine(kickpeers, name = 'kickpeers').switch()
586 DBM.addmon(NetworkManagerMonitor())
587 DBM.addmon(ConnManMonitor())
588 DBM.addmon(MaemoICdMonitor())
589 G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
594 Parse the command-line options.
596 Automatically changes directory to the requested configdir, and turns on
597 debugging. Returns the options object.
599 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
600 version = '%%prog %s' % VERSION)
602 op.add_option('-a', '--admin-socket',
603 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
604 help = 'Select socket to connect to [default %default]')
605 op.add_option('-d', '--directory',
606 metavar = 'DIR', dest = 'dir', default = T.configdir,
607 help = 'Select current diretory [default %default]')
608 op.add_option('-c', '--config',
609 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
610 help = 'Select configuration [default %default]')
611 op.add_option('--daemon', dest = 'daemon',
612 default = False, action = 'store_true',
613 help = 'Become a daemon after successful initialization')
614 op.add_option('--debug', dest = 'debug',
615 default = False, action = 'store_true',
616 help = 'Emit debugging trace information')
617 op.add_option('--startup', dest = 'startup',
618 default = False, action = 'store_true',
619 help = 'Being called as part of the server startup')
621 opts, args = op.parse_args()
622 if args: op.error('no arguments permitted')
624 T._debug = opts.debug
627 ## Service table, for running manually.
628 def cmd_updown(upness):
629 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
630 service_info = [('conntrack', VERSION, {
631 'up': (0, None, '', cmd_updown(True)),
632 'down': (0, None, '', cmd_updown(False)),
633 'show-config': (0, 0, '', cmd_showconfig),
634 'show-groups': (0, 0, '', cmd_showgroups),
635 'show-group': (1, 1, 'GROUP', cmd_showgroup)
638 if __name__ == '__main__':
639 opts = parse_options()
640 CF = Config(opts.conf)
641 T.runservices(opts.tripesock, service_info,
642 init = init, daemon = opts.daemon)
644 ###----- That's all, folks --------------------------------------------------