chiark / gitweb /
svc/conntrack.in: Keep the D-Bus monitor alive.
[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###
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.
17###
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.
22###
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.
26
27VERSION = '@VERSION@'
28
29###--------------------------------------------------------------------------
30### External dependencies.
31
32from ConfigParser import RawConfigParser
33from optparse import OptionParser
34import os as OS
35import sys as SYS
36import socket as S
37import mLib as M
38import tripe as T
39import dbus as D
40for i in ['mainloop', 'mainloop.glib']:
41 __import__('dbus.%s' % i)
42import gobject as G
43from struct import pack, unpack
44
45SM = T.svcmgr
46##__import__('rmcr').__debug = True
47
48###--------------------------------------------------------------------------
49### Utilities.
50
51class struct (object):
52 """A simple container object."""
53 def __init__(me, **kw):
54 me.__dict__.update(kw)
55
56def toposort(cmp, things):
57 """
58 Generate the THINGS in an order consistent with a given partial order.
59
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.
62
63 The THINGS may be any finite iterable; it is converted to a list
64 internally.
65 """
66
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
71 ## things.
72 things = list(things)
73 n = len(things)
74 order = [{} for i in xrange(n)]
75 rorder = [{} for i in xrange(n)]
76 for i in xrange(n):
77 for j in xrange(n):
78 if i != j and cmp(things[i], things[j]):
79 order[j][i] = True
80 rorder[i][j] = True
81
82 ## Now we can do the sort.
83 out = []
84 while True:
85 done = True
86 for i in xrange(n):
87 if order[i] is not None:
88 done = False
89 if len(order[i]) == 0:
90 for j in rorder[i]:
91 del order[j][i]
92 yield things[i]
93 order[i] = None
94 if done:
95 break
96
97###--------------------------------------------------------------------------
98### Parse the configuration file.
99
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.
104
105class Config (object):
106 """
107 Represents a configuration file.
108
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
116 ADDR/MASK.
117 """
118
119 def __init__(me, file):
120 """
121 Construct a new Config object, reading the given FILE.
122 """
123 me._file = file
124 me._fwatch = M.FWatch(file)
125 me._update()
126
127 def check(me):
128 """
129 See whether the configuration file has been updated.
130 """
131 if me._fwatch.update():
132 me._update()
133
134 def _update(me):
135 """
136 Internal function to update the configuration from the underlying file.
137 """
138
139 ## Read the configuration. We have no need of the fancy substitutions,
140 ## so turn them all off.
141 cp = RawConfigParser()
142 cp.read(me._file)
143
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)
151 else:
152 testaddr = '1.2.3.4'
153
154 ## Scan the configuration file and build the groups structure.
155 groups = []
156 for sec in cp.sections():
157 pats = []
158 for tag in cp.options(sec):
159 spec = cp.get(sec, tag).split()
160
161 ## Parse the entry into peer and network.
162 if len(spec) == 1:
163 peer = None
164 net = spec[0]
165 else:
166 peer, net = spec
167
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]))
175 else:
176 n = int(net[slash + 1:], 10)
177 mask = (1 << 32) - (1 << 32 - n)
178 pats.append((tag, peer, addr & mask, mask))
179
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): \
184 (p and not pp) or \
185 (p == pp and m == (m | mm) and aa == (a & mm)),
186 pats))
187 groups.append((sec, pats))
188
189 ## Done.
190 me.testaddr = testaddr
191 me.groups = groups
192
193### This will be a configuration file.
194CF = None
195
196###--------------------------------------------------------------------------
197### Responding to a network up/down event.
198
199def localaddr(peer):
200 """
201 Return the local IP address used for talking to PEER.
202 """
203 sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
204 try:
205 try:
206 sk.connect((peer, 1))
207 addr, _ = sk.getsockname()
208 addr, = unpack('>L', S.inet_aton(addr))
209 return addr
210 except S.error:
211 return None
212 finally:
213 sk.close()
214
215_kick = T.Queue()
216def kickpeers():
217 while True:
218 upness, reason = _kick.get()
219
220 ## Make sure the configuration file is up-to-date. Don't worry if we
221 ## can't do anything useful.
222 try:
223 CF.check()
224 except Exception, exc:
225 SM.warn('conntrack', 'config-file-error',
226 exc.__class__.__name__, str(exc))
227
228 ## Find the current list of peers.
229 peers = SM.list()
230
231 ## Work out the primary IP address.
232 if upness:
233 addr = localaddr(CF.testaddr)
234 if addr is None:
235 upness = False
236
237 ## Now decide what to do.
238 changes = []
239 for g, pp in CF.groups:
240
241 ## Find out which peer in the group ought to be active.
242 want = None # unequal to any string
243 if upness:
244 for t, p, a, m in pp:
245 if p is None:
246 aq = addr
247 else:
248 aq = localaddr(p)
249 if aq is not None and (aq & m) == a:
250 want = t
251 break
252
253 ## Shut down the wrong ones.
254 found = False
255 for p in peers:
256 if p == want:
257 found = True
258 elif p.startswith(g) and p != want:
259 changes.append(lambda p=p: SM.kill(p))
260
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',
264 want)))
265
266 ## Commit the changes.
267 if changes:
268 SM.notify('conntrack', upness and 'up' or 'down', *reason)
269 for c in changes: c()
270
271def netupdown(upness, reason):
272 """
273 Add or kill peers according to whether the network is up or down.
274
275 UPNESS is true if the network is up, or false if it's down.
276 """
277
278 _kick.put((upness, reason))
279
280###--------------------------------------------------------------------------
281### NetworkManager monitor.
282
283NM_NAME = 'org.freedesktop.NetworkManager'
284NM_PATH = '/org/freedesktop/NetworkManager'
285NM_IFACE = NM_NAME
286NMCA_IFACE = NM_NAME + '.Connection.Active'
287
288NM_STATE_CONNECTED = 3
289
290class NetworkManagerMonitor (object):
291 """
292 Watch NetworkManager signals for changes in network state.
293 """
294
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.
303
304 def attach(me, bus):
305 try:
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'])
310 else:
311 netupdown(False, ['nm', 'initially-disconnected'])
312 except D.DBusException:
313 pass
314 bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
315 NM_NAME, NM_PATH)
316 bus.add_signal_receiver(me._nm_connchange,
317 'PropertiesChanged', NMCA_IFACE,
318 NM_NAME, None)
319
320 def _nm_state(me, state):
321 if state == NM_STATE_CONNECTED:
322 netupdown(True, ['nm', 'connected'])
323 else:
324 netupdown(False, ['nm', 'disconnected'])
325
326 def _nm_connchange(me, props):
327 if props.get('Default', False):
328 netupdown(True, ['nm', 'default-connection-change'])
329
330###--------------------------------------------------------------------------
331### Maemo monitor.
332
333ICD_NAME = 'com.nokia.icd'
334ICD_PATH = '/com/nokia/icd'
335ICD_IFACE = ICD_NAME
336
337class MaemoICdMonitor (object):
338 """
339 Watch ICd signals for changes in network state.
340 """
341
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.
349
350 def attach(me, bus):
351 try:
352 icd = bus.get_object(ICD_NAME, ICD_PATH)
353 try:
354 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
355 me._iap = iap
356 netupdown(True, ['icd', 'initially-connected', iap])
357 except D.DBusException:
358 me._iap = None
359 netupdown(False, ['icd', 'initially-disconnected'])
360 except D.DBusException:
361 me._iap = None
362 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
363 ICD_NAME, ICD_PATH)
364
365 def _icd_state(me, iap, ty, state, hunoz):
366 if state == 'CONNECTED':
367 me._iap = iap
368 netupdown(True, ['icd', 'connected', iap])
369 elif state == 'IDLE' and iap == me._iap:
370 me._iap = None
371 netupdown(False, ['icd', 'idle'])
372
373###--------------------------------------------------------------------------
374### D-Bus connection tracking.
375
376class DBusMonitor (object):
377 """
378 Maintains a connection to the system D-Bus, and watches for signals.
379
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.
383 """
384
385 def __init__(me):
386 """
387 Initialise the object and try to establish a connection to the bus.
388 """
389 me._mons = []
390 me._loop = D.mainloop.glib.DBusGMainLoop()
391 me._reconnect()
392
393 def addmon(me, mon):
394 """
395 Add a monitor object to watch for signals.
396
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
399 relevant signals.
400 """
401 me._mons.append(mon)
402 if me._bus is not None:
403 mon.attach(me._bus)
404
405 def _reconnect(me):
406 """
407 Start connecting to the bus.
408
409 If we fail the first time, retry periodically.
410 """
411 me._bus = None
412 if me._try_connect():
413 G.timeout_add_seconds(5, me._try_connect)
414
415 def _try_connect(me):
416 """
417 Actually make a connection attempt.
418
419 If we succeed, attach the monitors.
420 """
421 try:
422 bus = D.SystemBus(mainloop = me._loop, private = True)
423 except D.DBusException:
424 return True
425 me._bus = bus
426 bus.call_on_disconnection(me._reconnect)
427 for m in me._mons:
428 m.attach(bus)
429 return False
430
431###--------------------------------------------------------------------------
432### TrIPE service.
433
434class GIOWatcher (object):
435 """
436 Monitor I/O events using glib.
437 """
438 def __init__(me, conn, mc = G.main_context_default()):
439 me._conn = conn
440 me._watch = None
441 me._mc = mc
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)
447 me._watch = None
448 def iterate(me):
449 me._mc.iteration(True)
450
451SM.iowatch = GIOWatcher(SM)
452
453def init():
454 """
455 Service initialization.
456
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.
459 """
29807d89 460 global DBM
22b47552 461 T.Coroutine(kickpeers, name = 'kickpeers').switch()
29807d89
MW
462 DBM = DBusMonitor()
463 DBM.addmon(NetworkManagerMonitor())
464 DBM.addmon(MaemoICdMonitor())
465 G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
466 or True))
2ec90437
MW
467
468def parse_options():
469 """
470 Parse the command-line options.
471
472 Automatically changes directory to the requested configdir, and turns on
473 debugging. Returns the options object.
474 """
475 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
476 version = '%%prog %s' % VERSION)
477
478 op.add_option('-a', '--admin-socket',
479 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
480 help = 'Select socket to connect to [default %default]')
481 op.add_option('-d', '--directory',
482 metavar = 'DIR', dest = 'dir', default = T.configdir,
483 help = 'Select current diretory [default %default]')
484 op.add_option('-c', '--config',
485 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
486 help = 'Select configuration [default %default]')
487 op.add_option('--daemon', dest = 'daemon',
488 default = False, action = 'store_true',
489 help = 'Become a daemon after successful initialization')
490 op.add_option('--debug', dest = 'debug',
491 default = False, action = 'store_true',
492 help = 'Emit debugging trace information')
493 op.add_option('--startup', dest = 'startup',
494 default = False, action = 'store_true',
495 help = 'Being called as part of the server startup')
496
497 opts, args = op.parse_args()
498 if args: op.error('no arguments permitted')
499 OS.chdir(opts.dir)
500 T._debug = opts.debug
501 return opts
502
503## Service table, for running manually.
504def cmd_updown(upness):
505 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
506service_info = [('conntrack', VERSION, {
507 'up': (0, None, '', cmd_updown(True)),
508 'down': (0, None, '', cmd_updown(False))
509})]
510
511if __name__ == '__main__':
512 opts = parse_options()
513 CF = Config(opts.conf)
514 T.runservices(opts.tripesock, service_info,
515 init = init, daemon = opts.daemon)
516
517###----- That's all, folks --------------------------------------------------