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)
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 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))
242 upness, reason = _kick.get()
243 if T._debug: print '# kickpeers %s: %s' % (upness, reason)
246 ## Make sure the configuration file is up-to-date. Don't worry if we
247 ## can't do anything useful.
250 except Exception, exc:
251 SM.warn('conntrack', 'config-file-error',
252 exc.__class__.__name__, str(exc))
254 ## Find the current list of peers.
257 ## Work out the primary IP address.
259 addr = localaddr(CF.testaddr)
264 if not T._debug: pass
265 elif addr: print '# local address = %s' % straddr(addr)
266 else: print '# offline'
268 ## Now decide what to do.
270 for g, pp in CF.groups:
271 if T._debug: print '# check group %s' % g
273 ## Find out which peer in the group ought to be active.
277 for t, p, a, m in pp:
278 if p is None or not upness:
283 info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
284 t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
285 if upness and ip is None and \
286 ipq is not None and (ipq & m) == a:
287 if T._debug: print '# %s: SELECTED' % info
289 select.append('%s=%s' % (g, t))
290 if t == 'down' or t.startswith('down/'):
297 if T._debug: print '# %s: skipped' % info
299 ## Shut down the wrong ones.
301 if T._debug: print '# peer-map = %r' % map
303 what = map.get(p, 'leave')
306 if T._debug: print '# peer %s: already up' % p
311 except T.TripeError, exc:
312 if exc.args[0] == 'unknown-peer':
313 ## Inherently racy; don't worry about this.
317 if T._debug: print '# peer %s: bring down' % p
320 ## Start the right one if necessary.
321 if want is not None and not found:
324 SM.svcsubmit('connect', 'active', want)
325 except T.TripeError, exc:
326 SM.warn('conntrack', 'connect-failed', want, *exc.args)
327 if T._debug: print '# peer %s: bring up' % want
330 ## Commit the changes.
332 SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
333 for c in changes: c()
335 def netupdown(upness, reason):
337 Add or kill peers according to whether the network is up or down.
339 UPNESS is true if the network is up, or false if it's down.
342 _kick.put((upness, reason))
344 ###--------------------------------------------------------------------------
345 ### NetworkManager monitor.
347 NM_NAME = 'org.freedesktop.NetworkManager'
348 NM_PATH = '/org/freedesktop/NetworkManager'
350 NMCA_IFACE = NM_NAME + '.Connection.Active'
352 NM_STATE_CONNECTED = 3
354 class NetworkManagerMonitor (object):
356 Watch NetworkManager signals for changes in network state.
359 ## Strategy. There are two kinds of interesting state transitions for us.
360 ## The first one is the global are-we-connected state, which we'll use to
361 ## toggle network upness on a global level. The second is which connection
362 ## has the default route, which we'll use to tweak which peer in the peer
363 ## group is active. The former is most easily tracked using the signal
364 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
365 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
366 ## look for when a new connection gains the default route.
370 nm = bus.get_object(NM_NAME, NM_PATH)
371 state = nm.Get(NM_IFACE, 'State')
372 if state == NM_STATE_CONNECTED:
373 netupdown(True, ['nm', 'initially-connected'])
375 netupdown(False, ['nm', 'initially-disconnected'])
376 except D.DBusException:
378 bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
380 bus.add_signal_receiver(me._nm_connchange,
381 'PropertiesChanged', NMCA_IFACE,
384 def _nm_state(me, state):
385 if state == NM_STATE_CONNECTED:
386 netupdown(True, ['nm', 'connected'])
388 netupdown(False, ['nm', 'disconnected'])
390 def _nm_connchange(me, props):
391 if props.get('Default', False):
392 netupdown(True, ['nm', 'default-connection-change'])
394 ###--------------------------------------------------------------------------
397 ICD_NAME = 'com.nokia.icd'
398 ICD_PATH = '/com/nokia/icd'
401 class MaemoICdMonitor (object):
403 Watch ICd signals for changes in network state.
406 ## Strategy. ICd only handles one connection at a time in steady state,
407 ## though when switching between connections, it tries to bring the new one
408 ## up before shutting down the old one. This makes life a bit easier than
409 ## it is with NetworkManager. On the other hand, the notifications are
410 ## relative to particular connections only, and the indicator that the old
411 ## connection is down (`IDLE') comes /after/ the new one comes up
412 ## (`CONNECTED'), so we have to remember which one is active.
416 icd = bus.get_object(ICD_NAME, ICD_PATH)
418 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
420 netupdown(True, ['icd', 'initially-connected', iap])
421 except D.DBusException:
423 netupdown(False, ['icd', 'initially-disconnected'])
424 except D.DBusException:
426 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
429 def _icd_state(me, iap, ty, state, hunoz):
430 if state == 'CONNECTED':
432 netupdown(True, ['icd', 'connected', iap])
433 elif state == 'IDLE' and iap == me._iap:
435 netupdown(False, ['icd', 'idle'])
437 ###--------------------------------------------------------------------------
438 ### D-Bus connection tracking.
440 class DBusMonitor (object):
442 Maintains a connection to the system D-Bus, and watches for signals.
444 If the connection is initially down, or drops for some reason, we retry
445 periodically (every five seconds at the moment). If the connection
446 resurfaces, we reattach the monitors.
451 Initialise the object and try to establish a connection to the bus.
454 me._loop = D.mainloop.glib.DBusGMainLoop()
455 me._state = 'startup'
460 Add a monitor object to watch for signals.
462 MON.attach(BUS) is called, with BUS being the connection to the system
463 bus. MON should query its service's current status and watch for
467 if me._bus is not None:
470 def _reconnect(me, hunoz = None):
472 Start connecting to the bus.
474 If we fail the first time, retry periodically.
476 if me._state == 'startup':
477 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
478 elif me._state == 'connected':
479 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
481 T.aside(SM.notify, 'conntrack', 'dbus-connection',
482 'state=%s' % me._state)
483 me._state == 'reconnecting'
485 if me._try_connect():
486 G.timeout_add_seconds(5, me._try_connect)
488 def _try_connect(me):
490 Actually make a connection attempt.
492 If we succeed, attach the monitors.
495 addr = OS.getenv('TRIPE_CONNTRACK_BUS')
496 if addr == 'SESSION':
497 bus = D.SessionBus(mainloop = me._loop, private = True)
498 elif addr is not None:
499 bus = D.bus.BusConnection(addr, mainloop = me._loop)
501 bus = D.SystemBus(mainloop = me._loop, private = True)
504 except D.DBusException, e:
507 me._state = 'connected'
508 bus.call_on_disconnection(me._reconnect)
509 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
512 ###--------------------------------------------------------------------------
515 class GIOWatcher (object):
517 Monitor I/O events using glib.
519 def __init__(me, conn, mc = G.main_context_default()):
523 def connected(me, sock):
524 me._watch = G.io_add_watch(sock, G.IO_IN,
525 lambda *hunoz: me._conn.receive())
526 def disconnected(me):
527 G.source_remove(me._watch)
530 me._mc.iteration(True)
532 SM.iowatch = GIOWatcher(SM)
536 Service initialization.
538 Add the D-Bus monitor here, because we might send commands off immediately,
539 and we want to make sure the server connection is up.
542 T.Coroutine(kickpeers, name = 'kickpeers').switch()
544 DBM.addmon(NetworkManagerMonitor())
545 DBM.addmon(MaemoICdMonitor())
546 G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
551 Parse the command-line options.
553 Automatically changes directory to the requested configdir, and turns on
554 debugging. Returns the options object.
556 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
557 version = '%%prog %s' % VERSION)
559 op.add_option('-a', '--admin-socket',
560 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
561 help = 'Select socket to connect to [default %default]')
562 op.add_option('-d', '--directory',
563 metavar = 'DIR', dest = 'dir', default = T.configdir,
564 help = 'Select current diretory [default %default]')
565 op.add_option('-c', '--config',
566 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
567 help = 'Select configuration [default %default]')
568 op.add_option('--daemon', dest = 'daemon',
569 default = False, action = 'store_true',
570 help = 'Become a daemon after successful initialization')
571 op.add_option('--debug', dest = 'debug',
572 default = False, action = 'store_true',
573 help = 'Emit debugging trace information')
574 op.add_option('--startup', dest = 'startup',
575 default = False, action = 'store_true',
576 help = 'Being called as part of the server startup')
578 opts, args = op.parse_args()
579 if args: op.error('no arguments permitted')
581 T._debug = opts.debug
584 ## Service table, for running manually.
585 def cmd_updown(upness):
586 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
587 service_info = [('conntrack', VERSION, {
588 'up': (0, None, '', cmd_updown(True)),
589 'down': (0, None, '', cmd_updown(False)),
590 'show-config': (0, 0, '', cmd_showconfig),
591 'show-groups': (0, 0, '', cmd_showgroups),
592 'show-group': (1, 1, 'GROUP', cmd_showgroup)
595 if __name__ == '__main__':
596 opts = parse_options()
597 CF = Config(opts.conf)
598 T.runservices(opts.tripesock, service_info,
599 init = init, daemon = opts.daemon)
601 ###----- That's all, folks --------------------------------------------------