chiark / gitweb /
svc/conntrack.in: Fix netmask parsing.
[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###
11ad66c2
MW
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.
2ec90437 17###
11ad66c2
MW
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
21### for more details.
2ec90437
MW
22###
23### You should have received a copy of the GNU General Public License
11ad66c2 24### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
2ec90437
MW
25
26VERSION = '@VERSION@'
27
28###--------------------------------------------------------------------------
29### External dependencies.
30
31from ConfigParser import RawConfigParser
32from optparse import OptionParser
33import os as OS
34import sys as SYS
35import socket as S
36import mLib as M
37import tripe as T
38import dbus as D
39for i in ['mainloop', 'mainloop.glib']:
40 __import__('dbus.%s' % i)
a69f4417
MW
41try: from gi.repository import GLib as G
42except ImportError: import gobject as G
2ec90437
MW
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)
2d4998c4 143 if T._debug: print '# reread config'
2ec90437
MW
144
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)
152 else:
153 testaddr = '1.2.3.4'
154
155 ## Scan the configuration file and build the groups structure.
156 groups = []
157 for sec in cp.sections():
158 pats = []
159 for tag in cp.options(sec):
160 spec = cp.get(sec, tag).split()
161
162 ## Parse the entry into peer and network.
163 if len(spec) == 1:
164 peer = None
165 net = spec[0]
166 else:
167 peer, net = spec
168
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]))
508294ea 174 if net[slash + 1:].isdigit():
2ec90437
MW
175 n = int(net[slash + 1:], 10)
176 mask = (1 << 32) - (1 << 32 - n)
508294ea
MW
177 else:
178 mask, = unpack('>L', S.inet_aton(net[slash + 1:]))
2ec90437
MW
179 pats.append((tag, peer, addr & mask, mask))
180
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): \
185 (p and not pp) or \
186 (p == pp and m == (m | mm) and aa == (a & mm)),
187 pats))
188 groups.append((sec, pats))
189
190 ## Done.
191 me.testaddr = testaddr
192 me.groups = groups
193
194### This will be a configuration file.
195CF = None
196
c3897a7d 197def straddr(a): return a is None and '#<none>' or S.inet_ntoa(pack('>L', a))
2d4998c4
MW
198def strmask(m):
199 for i in xrange(33):
200 if m == 0xffffffff ^ ((1 << (32 - i)) - 1): return i
201 return straddr(m)
202
203def cmd_showconfig():
204 T.svcinfo('test-addr=%s' % CF.testaddr)
205def cmd_showgroups():
206 for sec, pats in CF.groups:
207 T.svcinfo(sec)
208def cmd_showgroup(g):
209 for s, p in CF.groups:
210 if s == g:
211 pats = p
212 break
213 else:
171206b5 214 raise T.TripeJobError('unknown-group', g)
2d4998c4
MW
215 for t, p, a, m in pats:
216 T.svcinfo('peer', t,
217 'target', p or '(default)',
218 'net', '%s/%s' % (straddr(a), strmask(m)))
219
2ec90437
MW
220###--------------------------------------------------------------------------
221### Responding to a network up/down event.
222
223def localaddr(peer):
224 """
225 Return the local IP address used for talking to PEER.
226 """
227 sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
228 try:
229 try:
230 sk.connect((peer, 1))
231 addr, _ = sk.getsockname()
232 addr, = unpack('>L', S.inet_aton(addr))
233 return addr
234 except S.error:
235 return None
236 finally:
237 sk.close()
238
239_kick = T.Queue()
f5393555
MW
240_delay = None
241
242def cancel_delay():
243 global _delay
244 if _delay is not None:
245 if T._debug: print '# cancel delayed kick'
246 G.source_remove(_delay)
247 _delay = None
4f6b41b9
MW
248
249def netupdown(upness, reason):
250 """
251 Add or kill peers according to whether the network is up or down.
252
253 UPNESS is true if the network is up, or false if it's down.
254 """
255
256 _kick.put((upness, reason))
257
f5393555
MW
258def delay_netupdown(upness, reason):
259 global _delay
260 cancel_delay()
261 def _func():
262 global _delay
263 if T._debug: print '# delayed %s: %s' % (upness, reason)
264 _delay = None
265 netupdown(upness, reason)
266 return False
267 if T._debug: print '# delaying %s: %s' % (upness, reason)
268 _delay = G.timeout_add(2000, _func)
269
2ec90437
MW
270def kickpeers():
271 while True:
272 upness, reason = _kick.get()
2d4998c4
MW
273 if T._debug: print '# kickpeers %s: %s' % (upness, reason)
274 select = []
f5393555 275 cancel_delay()
2ec90437
MW
276
277 ## Make sure the configuration file is up-to-date. Don't worry if we
278 ## can't do anything useful.
279 try:
280 CF.check()
281 except Exception, exc:
282 SM.warn('conntrack', 'config-file-error',
283 exc.__class__.__name__, str(exc))
284
285 ## Find the current list of peers.
286 peers = SM.list()
287
288 ## Work out the primary IP address.
289 if upness:
290 addr = localaddr(CF.testaddr)
291 if addr is None:
292 upness = False
b10a8c3d
MW
293 else:
294 addr = None
2d4998c4
MW
295 if not T._debug: pass
296 elif addr: print '# local address = %s' % straddr(addr)
297 else: print '# offline'
2ec90437
MW
298
299 ## Now decide what to do.
300 changes = []
301 for g, pp in CF.groups:
2d4998c4 302 if T._debug: print '# check group %s' % g
2ec90437
MW
303
304 ## Find out which peer in the group ought to be active.
b10a8c3d
MW
305 ip = None
306 map = {}
307 want = None
308 for t, p, a, m in pp:
309 if p is None or not upness:
310 ipq = addr
311 else:
312 ipq = localaddr(p)
2d4998c4
MW
313 if T._debug:
314 info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
315 t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
b10a8c3d
MW
316 if upness and ip is None and \
317 ipq is not None and (ipq & m) == a:
2d4998c4 318 if T._debug: print '# %s: SELECTED' % info
b10a8c3d 319 map[t] = 'up'
2d4998c4 320 select.append('%s=%s' % (g, t))
f2bdb96e
MW
321 if t == 'down' or t.startswith('down/'):
322 want = None
323 else:
324 want = t
b10a8c3d
MW
325 ip = ipq
326 else:
327 map[t] = 'down'
2d4998c4 328 if T._debug: print '# %s: skipped' % info
2ec90437
MW
329
330 ## Shut down the wrong ones.
331 found = False
2d4998c4 332 if T._debug: print '# peer-map = %r' % map
2ec90437 333 for p in peers:
b10a8c3d
MW
334 what = map.get(p, 'leave')
335 if what == 'up':
2ec90437 336 found = True
2d4998c4 337 if T._debug: print '# peer %s: already up' % p
b10a8c3d 338 elif what == 'down':
cf2e4ea6
MW
339 def _(p = p):
340 try:
341 SM.kill(p)
342 except T.TripeError, exc:
343 if exc.args[0] == 'unknown-peer':
344 ## Inherently racy; don't worry about this.
345 pass
346 else:
347 raise
2d4998c4 348 if T._debug: print '# peer %s: bring down' % p
cf2e4ea6 349 changes.append(_)
2ec90437
MW
350
351 ## Start the right one if necessary.
7b7e3c74 352 if want is not None and not found:
cf2e4ea6
MW
353 def _(want = want):
354 try:
8d1d183e 355 list(SM.svcsubmit('connect', 'active', want))
cf2e4ea6
MW
356 except T.TripeError, exc:
357 SM.warn('conntrack', 'connect-failed', want, *exc.args)
2d4998c4 358 if T._debug: print '# peer %s: bring up' % want
cf2e4ea6 359 changes.append(_)
2ec90437
MW
360
361 ## Commit the changes.
362 if changes:
2d4998c4 363 SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
2ec90437
MW
364 for c in changes: c()
365
2ec90437
MW
366###--------------------------------------------------------------------------
367### NetworkManager monitor.
368
498d9f42
MW
369DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
370
2ec90437
MW
371NM_NAME = 'org.freedesktop.NetworkManager'
372NM_PATH = '/org/freedesktop/NetworkManager'
373NM_IFACE = NM_NAME
374NMCA_IFACE = NM_NAME + '.Connection.Active'
375
2079efa1
MW
376NM_STATE_CONNECTED = 3 #obsolete
377NM_STATE_CONNECTED_LOCAL = 50
378NM_STATE_CONNECTED_SITE = 60
379NM_STATE_CONNECTED_GLOBAL = 70
380NM_CONNSTATES = set([NM_STATE_CONNECTED,
381 NM_STATE_CONNECTED_LOCAL,
382 NM_STATE_CONNECTED_SITE,
383 NM_STATE_CONNECTED_GLOBAL])
2ec90437
MW
384
385class NetworkManagerMonitor (object):
386 """
387 Watch NetworkManager signals for changes in network state.
388 """
389
390 ## Strategy. There are two kinds of interesting state transitions for us.
391 ## The first one is the global are-we-connected state, which we'll use to
392 ## toggle network upness on a global level. The second is which connection
393 ## has the default route, which we'll use to tweak which peer in the peer
394 ## group is active. The former is most easily tracked using the signal
395 ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
396 ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
397 ## look for when a new connection gains the default route.
398
399 def attach(me, bus):
400 try:
401 nm = bus.get_object(NM_NAME, NM_PATH)
498d9f42 402 state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE)
2079efa1 403 if state in NM_CONNSTATES:
2ec90437
MW
404 netupdown(True, ['nm', 'initially-connected'])
405 else:
406 netupdown(False, ['nm', 'initially-disconnected'])
bd9bd714
MW
407 except D.DBusException, e:
408 if T._debug: print '# exception attaching to network-manager: %s' % e
2079efa1
MW
409 bus.add_signal_receiver(me._nm_state, 'StateChanged',
410 NM_IFACE, NM_NAME, NM_PATH)
411 bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
412 NMCA_IFACE, NM_NAME, None)
2ec90437
MW
413
414 def _nm_state(me, state):
2079efa1 415 if state in NM_CONNSTATES:
f5393555 416 delay_netupdown(True, ['nm', 'connected'])
2ec90437 417 else:
f5393555 418 delay_netupdown(False, ['nm', 'disconnected'])
2ec90437
MW
419
420 def _nm_connchange(me, props):
f5393555
MW
421 if props.get('Default', False) or props.get('Default6', False):
422 delay_netupdown(True, ['nm', 'default-connection-change'])
2ec90437 423
a95eb44a
MW
424##--------------------------------------------------------------------------
425### Connman monitor.
426
427CM_NAME = 'net.connman'
428CM_PATH = '/'
429CM_IFACE = 'net.connman.Manager'
430
431class ConnManMonitor (object):
432 """
433 Watch ConnMan signls for changes in network state.
434 """
435
436 ## Strategy. Everything seems to be usefully encoded in the `State'
437 ## property. If it's `offline', `idle' or `ready' then we don't expect a
438 ## network connection. During handover from one network to another, the
439 ## property passes through `ready' to `online'.
440
441 def attach(me, bus):
442 try:
443 cm = bus.get_object(CM_NAME, CM_PATH)
444 props = cm.GetProperties(dbus_interface = CM_IFACE)
445 state = props['State']
446 netupdown(state == 'online', ['connman', 'initially-%s' % state])
bd9bd714
MW
447 except D.DBusException, e:
448 if T._debug: print '# exception attaching to connman: %s' % e
a95eb44a
MW
449 bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
450 CM_IFACE, CM_NAME, CM_PATH)
451
452 def _cm_state(me, prop, value):
453 if prop != 'State': return
f5393555 454 delay_netupdown(value == 'online', ['connman', value])
a95eb44a 455
2ec90437
MW
456###--------------------------------------------------------------------------
457### Maemo monitor.
458
459ICD_NAME = 'com.nokia.icd'
460ICD_PATH = '/com/nokia/icd'
461ICD_IFACE = ICD_NAME
462
463class MaemoICdMonitor (object):
464 """
465 Watch ICd signals for changes in network state.
466 """
467
468 ## Strategy. ICd only handles one connection at a time in steady state,
469 ## though when switching between connections, it tries to bring the new one
470 ## up before shutting down the old one. This makes life a bit easier than
471 ## it is with NetworkManager. On the other hand, the notifications are
472 ## relative to particular connections only, and the indicator that the old
473 ## connection is down (`IDLE') comes /after/ the new one comes up
474 ## (`CONNECTED'), so we have to remember which one is active.
475
476 def attach(me, bus):
477 try:
478 icd = bus.get_object(ICD_NAME, ICD_PATH)
479 try:
480 iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
481 me._iap = iap
482 netupdown(True, ['icd', 'initially-connected', iap])
483 except D.DBusException:
484 me._iap = None
485 netupdown(False, ['icd', 'initially-disconnected'])
bd9bd714
MW
486 except D.DBusException, e:
487 if T._debug: print '# exception attaching to icd: %s' % e
2ec90437
MW
488 me._iap = None
489 bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
490 ICD_NAME, ICD_PATH)
491
492 def _icd_state(me, iap, ty, state, hunoz):
493 if state == 'CONNECTED':
494 me._iap = iap
f5393555 495 delay_netupdown(True, ['icd', 'connected', iap])
2ec90437
MW
496 elif state == 'IDLE' and iap == me._iap:
497 me._iap = None
f5393555 498 delay_netupdown(False, ['icd', 'idle'])
2ec90437
MW
499
500###--------------------------------------------------------------------------
501### D-Bus connection tracking.
502
503class DBusMonitor (object):
504 """
505 Maintains a connection to the system D-Bus, and watches for signals.
506
507 If the connection is initially down, or drops for some reason, we retry
508 periodically (every five seconds at the moment). If the connection
509 resurfaces, we reattach the monitors.
510 """
511
512 def __init__(me):
513 """
514 Initialise the object and try to establish a connection to the bus.
515 """
516 me._mons = []
517 me._loop = D.mainloop.glib.DBusGMainLoop()
7bfa1e06 518 me._state = 'startup'
2ec90437
MW
519 me._reconnect()
520
521 def addmon(me, mon):
522 """
523 Add a monitor object to watch for signals.
524
525 MON.attach(BUS) is called, with BUS being the connection to the system
526 bus. MON should query its service's current status and watch for
527 relevant signals.
528 """
529 me._mons.append(mon)
530 if me._bus is not None:
531 mon.attach(me._bus)
532
16650038 533 def _reconnect(me, hunoz = None):
2ec90437
MW
534 """
535 Start connecting to the bus.
536
537 If we fail the first time, retry periodically.
538 """
7bfa1e06
MW
539 if me._state == 'startup':
540 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
541 elif me._state == 'connected':
542 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
543 else:
544 T.aside(SM.notify, 'conntrack', 'dbus-connection',
545 'state=%s' % me._state)
546 me._state == 'reconnecting'
2ec90437
MW
547 me._bus = None
548 if me._try_connect():
549 G.timeout_add_seconds(5, me._try_connect)
550
551 def _try_connect(me):
552 """
553 Actually make a connection attempt.
554
555 If we succeed, attach the monitors.
556 """
557 try:
7bfa1e06
MW
558 addr = OS.getenv('TRIPE_CONNTRACK_BUS')
559 if addr == 'SESSION':
560 bus = D.SessionBus(mainloop = me._loop, private = True)
561 elif addr is not None:
562 bus = D.bus.BusConnection(addr, mainloop = me._loop)
563 else:
564 bus = D.SystemBus(mainloop = me._loop, private = True)
565 for m in me._mons:
566 m.attach(bus)
567 except D.DBusException, e:
2ec90437
MW
568 return True
569 me._bus = bus
7bfa1e06 570 me._state = 'connected'
2ec90437 571 bus.call_on_disconnection(me._reconnect)
7bfa1e06 572 T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
2ec90437
MW
573 return False
574
575###--------------------------------------------------------------------------
576### TrIPE service.
577
578class GIOWatcher (object):
579 """
580 Monitor I/O events using glib.
581 """
582 def __init__(me, conn, mc = G.main_context_default()):
583 me._conn = conn
584 me._watch = None
585 me._mc = mc
586 def connected(me, sock):
587 me._watch = G.io_add_watch(sock, G.IO_IN,
588 lambda *hunoz: me._conn.receive())
589 def disconnected(me):
590 G.source_remove(me._watch)
591 me._watch = None
592 def iterate(me):
593 me._mc.iteration(True)
594
595SM.iowatch = GIOWatcher(SM)
596
597def init():
598 """
599 Service initialization.
600
601 Add the D-Bus monitor here, because we might send commands off immediately,
602 and we want to make sure the server connection is up.
603 """
29807d89 604 global DBM
22b47552 605 T.Coroutine(kickpeers, name = 'kickpeers').switch()
29807d89
MW
606 DBM = DBusMonitor()
607 DBM.addmon(NetworkManagerMonitor())
a95eb44a 608 DBM.addmon(ConnManMonitor())
29807d89 609 DBM.addmon(MaemoICdMonitor())
f5393555
MW
610 G.timeout_add_seconds(30, lambda: (_delay is not None or
611 netupdown(True, ['interval-timer']) or
612 True))
2ec90437
MW
613
614def parse_options():
615 """
616 Parse the command-line options.
617
618 Automatically changes directory to the requested configdir, and turns on
619 debugging. Returns the options object.
620 """
621 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
622 version = '%%prog %s' % VERSION)
623
624 op.add_option('-a', '--admin-socket',
625 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
626 help = 'Select socket to connect to [default %default]')
627 op.add_option('-d', '--directory',
628 metavar = 'DIR', dest = 'dir', default = T.configdir,
629 help = 'Select current diretory [default %default]')
630 op.add_option('-c', '--config',
631 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
632 help = 'Select configuration [default %default]')
633 op.add_option('--daemon', dest = 'daemon',
634 default = False, action = 'store_true',
635 help = 'Become a daemon after successful initialization')
636 op.add_option('--debug', dest = 'debug',
637 default = False, action = 'store_true',
638 help = 'Emit debugging trace information')
639 op.add_option('--startup', dest = 'startup',
640 default = False, action = 'store_true',
641 help = 'Being called as part of the server startup')
642
643 opts, args = op.parse_args()
644 if args: op.error('no arguments permitted')
645 OS.chdir(opts.dir)
646 T._debug = opts.debug
647 return opts
648
649## Service table, for running manually.
650def cmd_updown(upness):
651 return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
652service_info = [('conntrack', VERSION, {
653 'up': (0, None, '', cmd_updown(True)),
2d4998c4
MW
654 'down': (0, None, '', cmd_updown(False)),
655 'show-config': (0, 0, '', cmd_showconfig),
656 'show-groups': (0, 0, '', cmd_showgroups),
657 'show-group': (1, 1, 'GROUP', cmd_showgroup)
2ec90437
MW
658})]
659
660if __name__ == '__main__':
661 opts = parse_options()
662 CF = Config(opts.conf)
663 T.runservices(opts.tripesock, service_info,
664 init = init, daemon = opts.daemon)
665
666###----- That's all, folks --------------------------------------------------