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()
144 ## Save the test address. Make sure it's vaguely sensible. The default
145 ## is probably good for most cases, in fact, since that address isn't
146 ## actually in use. Note that we never send packets to the test address;
147 ## we just use it to discover routing information.
148 if cp.has_option('DEFAULT', 'test-addr'):
149 testaddr = cp.get('DEFAULT', 'test-addr')
150 S.inet_aton(testaddr)
154 ## Scan the configuration file and build the groups structure.
156 for sec in cp.sections():
158 for tag in cp.options(sec):
159 spec = cp.get(sec, tag).split()
161 ## Parse the entry into peer and network.
168 ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
169 ## and MASK is either a dotted-quad or a single integer N indicating
170 ## a mask with N leading ones followed by trailing zeroes.
171 slash = net.index('/')
172 addr, = unpack('>L', S.inet_aton(net[:slash]))
173 if net.find('.', slash + 1) >= 0:
174 mask, = unpack('>L', S.inet_aton(net[:slash]))
176 n = int(net[slash + 1:], 10)
177 mask = (1 << 32) - (1 << 32 - n)
178 pats.append((tag, peer, addr & mask, mask))
180 ## Annoyingly, RawConfigParser doesn't preserve the order of options.
181 ## In order to make things vaguely sane, we topologically sort the
182 ## patterns so that more specific patterns are checked first.
183 pats = list(toposort(lambda (t, p, a, m), (tt, pp, aa, mm): \
185 (p == pp and m == (m | mm) and aa == (a & mm)),
187 groups.append((sec, pats))
190 me.testaddr = testaddr
193 ### This will be a configuration file.
196 ###--------------------------------------------------------------------------
197 ### Responding to a network up/down event.
201 Return the local IP address used for talking to PEER.
203 sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
206 sk.connect((peer, 1))
207 addr, _ = sk.getsockname()
208 addr, = unpack('>L', S.inet_aton(addr))
218 upness, reason = _kick.get()
220 ## Make sure the configuration file is up-to-date. Don't worry if we
221 ## can't do anything useful.
224 except Exception, exc:
225 SM.warn('conntrack', 'config-file-error',
226 exc.__class__.__name__, str(exc))
228 ## Find the current list of peers.
231 ## Work out the primary IP address.
233 addr = localaddr(CF.testaddr)
237 ## Now decide what to do.
239 for g, pp in CF.groups:
241 ## Find out which peer in the group ought to be active.
242 want = None # unequal to any string
244 for t, p, a, m in pp:
249 if aq is not None and (aq & m) == a:
253 ## Shut down the wrong ones.
258 elif p.startswith(g) and p != want:
259 changes.append(lambda p=p: SM.kill(p))
261 ## Start the right one if necessary.
262 if want is not None and not found:
263 changes.append(lambda: T._simple(SM.svcsubmit('connect', 'active',
266 ## Commit the changes.
268 SM.notify('conntrack', upness and 'up' or 'down', *reason)
269 for c in changes: c()
271 def netupdown(upness, reason):
273 Add or kill peers according to whether the network is up or down.
275 UPNESS is true if the network is up, or false if it's down.
278 _kick.put((upness, reason))
280 ###--------------------------------------------------------------------------
281 ### NetworkManager monitor.
283 NM_NAME = 'org.freedesktop.NetworkManager'
284 NM_PATH = '/org/freedesktop/NetworkManager'
286 NMCA_IFACE = NM_NAME + '.Connection.Active'
288 NM_STATE_CONNECTED = 3
290 class NetworkManagerMonitor (object):
292 Watch NetworkManager signals for changes in network state.
295 ## Strategy. There are two kinds of interesting state transitions for us.
296 ## The first one is the global are-we-connected state, which we'll use to
297 ## toggle network upness on a global level. The second is which connection
298 ## has the default route, which we'll use to tweak which peer in the peer
299 ## group is active. The former is most easily tracked using the signal
300 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
301 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
302 ## look for when a new connection gains the default route.
306 nm = bus.get_object(NM_NAME, NM_PATH)
307 state = nm.Get(NM_IFACE, 'State')
308 if state == NM_STATE_CONNECTED:
309 netupdown(True, ['nm', 'initially-connected'])
311 netupdown(False, ['nm', 'initially-disconnected'])
312 except D.DBusException:
314 bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
316 bus.add_signal_receiver(me._nm_connchange,
317 'PropertiesChanged', NMCA_IFACE,
320 def _nm_state(me, state):
321 if state == NM_STATE_CONNECTED:
322 netupdown(True, ['nm', 'connected'])
324 netupdown(False, ['nm', 'disconnected'])
326 def _nm_connchange(me, props):
327 if props.get('Default', False):
328 netupdown(True, ['nm', 'default-connection-change'])
330 ###--------------------------------------------------------------------------
333 ICD_NAME = 'com.nokia.icd'
334 ICD_PATH = '/com/nokia/icd'
337 class MaemoICdMonitor (object):
339 Watch ICd signals for changes in network state.
342 ## Strategy. ICd only handles one connection at a time in steady state,
343 ## though when switching between connections, it tries to bring the new one
344 ## up before shutting down the old one. This makes life a bit easier than
345 ## it is with NetworkManager. On the other hand, the notifications are
346 ## relative to particular connections only, and the indicator that the old
347 ## connection is down (`IDLE') comes /after/ the new one comes up
348 ## (`CONNECTED'), so we have to remember which one is active.
352 icd = bus.get_object(ICD_NAME, ICD_PATH)
354 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
356 netupdown(True, ['icd', 'initially-connected', iap])
357 except D.DBusException:
359 netupdown(False, ['icd', 'initially-disconnected'])
360 except D.DBusException:
362 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
365 def _icd_state(me, iap, ty, state, hunoz):
366 if state == 'CONNECTED':
368 netupdown(True, ['icd', 'connected', iap])
369 elif state == 'IDLE' and iap == me._iap:
371 netupdown(False, ['icd', 'idle'])
373 ###--------------------------------------------------------------------------
374 ### D-Bus connection tracking.
376 class DBusMonitor (object):
378 Maintains a connection to the system D-Bus, and watches for signals.
380 If the connection is initially down, or drops for some reason, we retry
381 periodically (every five seconds at the moment). If the connection
382 resurfaces, we reattach the monitors.
387 Initialise the object and try to establish a connection to the bus.
390 me._loop = D.mainloop.glib.DBusGMainLoop()
395 Add a monitor object to watch for signals.
397 MON.attach(BUS) is called, with BUS being the connection to the system
398 bus. MON should query its service's current status and watch for
402 if me._bus is not None:
407 Start connecting to the bus.
409 If we fail the first time, retry periodically.
412 if me._try_connect():
413 G.timeout_add_seconds(5, me._try_connect)
415 def _try_connect(me):
417 Actually make a connection attempt.
419 If we succeed, attach the monitors.
422 bus = D.SystemBus(mainloop = me._loop, private = True)
423 except D.DBusException:
426 bus.call_on_disconnection(me._reconnect)
431 ###--------------------------------------------------------------------------
434 class GIOWatcher (object):
436 Monitor I/O events using glib.
438 def __init__(me, conn, mc = G.main_context_default()):
442 def connected(me, sock):
443 me._watch = G.io_add_watch(sock, G.IO_IN,
444 lambda *hunoz: me._conn.receive())
445 def disconnected(me):
446 G.source_remove(me._watch)
449 me._mc.iteration(True)
451 SM.iowatch = GIOWatcher(SM)
455 Service initialization.
457 Add the D-Bus monitor here, because we might send commands off immediately,
458 and we want to make sure the server connection is up.
460 T.Coroutine(kickpeers).switch()
462 dbm.addmon(NetworkManagerMonitor())
463 dbm.addmon(MaemoICdMonitor())
464 G.timeout_add_seconds(300, lambda: (netupdown(True, ['interval-timer'])
469 Parse the command-line options.
471 Automatically changes directory to the requested configdir, and turns on
472 debugging. Returns the options object.
474 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
475 version = '%%prog %s' % VERSION)
477 op.add_option('-a', '--admin-socket',
478 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
479 help = 'Select socket to connect to [default %default]')
480 op.add_option('-d', '--directory',
481 metavar = 'DIR', dest = 'dir', default = T.configdir,
482 help = 'Select current diretory [default %default]')
483 op.add_option('-c', '--config',
484 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
485 help = 'Select configuration [default %default]')
486 op.add_option('--daemon', dest = 'daemon',
487 default = False, action = 'store_true',
488 help = 'Become a daemon after successful initialization')
489 op.add_option('--debug', dest = 'debug',
490 default = False, action = 'store_true',
491 help = 'Emit debugging trace information')
492 op.add_option('--startup', dest = 'startup',
493 default = False, action = 'store_true',
494 help = 'Being called as part of the server startup')
496 opts, args = op.parse_args()
497 if args: op.error('no arguments permitted')
499 T._debug = opts.debug
502 ## Service table, for running manually.
503 def cmd_updown(upness):
504 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
505 service_info = [('conntrack', VERSION, {
506 'up': (0, None, '', cmd_updown(True)),
507 'down': (0, None, '', cmd_updown(False))
510 if __name__ == '__main__':
511 opts = parse_options()
512 CF = Config(opts.conf)
513 T.runservices(opts.tripesock, service_info,
514 init = init, daemon = opts.daemon)
516 ###----- That's all, folks --------------------------------------------------