svc/conntrack.in: Introduce a function for parsing address strings.
[tripe] / svc / conntrack.in
index a743132..061d6d7 100644 (file)
 ###
 ### This file is part of Trivial IP Encryption (TrIPE).
 ###
-### TrIPE is free software; you can redistribute it and/or modify
-### it under the terms of the GNU General Public License as published by
-### the Free Software Foundation; either version 2 of the License, or
-### (at your option) any later version.
+### TrIPE is free software: you can redistribute it and/or modify it under
+### the terms of the GNU General Public License as published by the Free
+### Software Foundation; either version 3 of the License, or (at your
+### option) any later version.
 ###
-### TrIPE is distributed in the hope that it will be useful,
-### but WITHOUT ANY WARRANTY; without even the implied warranty of
-### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-### GNU General Public License for more details.
+### TrIPE is distributed in the hope that it will be useful, but WITHOUT
+### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+### for more details.
 ###
 ### You should have received a copy of the GNU General Public License
-### along with TrIPE; if not, write to the Free Software Foundation,
-### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+### along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
 
 VERSION = '@VERSION@'
 
@@ -39,7 +38,8 @@ import tripe as T
 import dbus as D
 for i in ['mainloop', 'mainloop.glib']:
   __import__('dbus.%s' % i)
-import gobject as G
+try: from gi.repository import GLib as G
+except ImportError: import gobject as G
 from struct import pack, unpack
 
 SM = T.svcmgr
@@ -94,6 +94,9 @@ def toposort(cmp, things):
     if done:
       break
 
+def parse_address(addrstr):
+  return unpack('>L', S.inet_aton(addrstr))[0]
+
 ###--------------------------------------------------------------------------
 ### Parse the configuration file.
 
@@ -140,6 +143,7 @@ class Config (object):
     ## so turn them all off.
     cp = RawConfigParser()
     cp.read(me._file)
+    if T._debug: print '# reread config'
 
     ## Save the test address.  Make sure it's vaguely sensible.  The default
     ## is probably good for most cases, in fact, since that address isn't
@@ -169,12 +173,12 @@ class Config (object):
         ## and MASK is either a dotted-quad or a single integer N indicating
         ## a mask with N leading ones followed by trailing zeroes.
         slash = net.index('/')
-        addr, = unpack('>L', S.inet_aton(net[:slash]))
-        if net.find('.', slash + 1) >= 0:
-          mask, = unpack('>L', S.inet_aton(net[:slash]))
-        else:
+        addr = parse_address(net[:slash])
+        if net[slash + 1:].isdigit():
           n = int(net[slash + 1:], 10)
           mask = (1 << 32) - (1 << 32 - n)
+        else:
+          mask = parse_address(net[slash + 1:])
         pats.append((tag, peer, addr & mask, mask))
 
       ## Annoyingly, RawConfigParser doesn't preserve the order of options.
@@ -193,6 +197,29 @@ class Config (object):
 ### This will be a configuration file.
 CF = None
 
+def straddr(a): return a is None and '#<none>' or S.inet_ntoa(pack('>L', a))
+def strmask(m):
+  for i in xrange(33):
+    if m == 0xffffffff ^ ((1 << (32 - i)) - 1): return str(i)
+  return straddr(m)
+
+def cmd_showconfig():
+  T.svcinfo('test-addr=%s' % CF.testaddr)
+def cmd_showgroups():
+  for sec, pats in CF.groups:
+    T.svcinfo(sec)
+def cmd_showgroup(g):
+  for s, p in CF.groups:
+    if s == g:
+      pats = p
+      break
+  else:
+    raise T.TripeJobError('unknown-group', g)
+  for t, p, a, m in pats:
+    T.svcinfo('peer', t,
+              'target', p or '(default)',
+              'net', '%s/%s' % (straddr(a), strmask(m)))
+
 ###--------------------------------------------------------------------------
 ### Responding to a network up/down event.
 
@@ -205,7 +232,7 @@ def localaddr(peer):
     try:
       sk.connect((peer, 1))
       addr, _ = sk.getsockname()
-      addr, = unpack('>L', S.inet_aton(addr))
+      addr = parse_address(addr)
       return addr
     except S.error:
       return None
@@ -213,10 +240,42 @@ def localaddr(peer):
     sk.close()
 
 _kick = T.Queue()
+_delay = None
+
+def cancel_delay():
+  global _delay
+  if _delay is not None:
+    if T._debug: print '# cancel delayed kick'
+    G.source_remove(_delay)
+    _delay = None
+
+def netupdown(upness, reason):
+  """
+  Add or kill peers according to whether the network is up or down.
+
+  UPNESS is true if the network is up, or false if it's down.
+  """
+
+  _kick.put((upness, reason))
+
+def delay_netupdown(upness, reason):
+  global _delay
+  cancel_delay()
+  def _func():
+    global _delay
+    if T._debug: print '# delayed %s: %s' % (upness, reason)
+    _delay = None
+    netupdown(upness, reason)
+    return False
+  if T._debug: print '# delaying %s: %s' % (upness, reason)
+  _delay = G.timeout_add(2000, _func)
+
 def kickpeers():
-  lastip = {}
   while True:
     upness, reason = _kick.get()
+    if T._debug: print '# kickpeers %s: %s' % (upness, reason)
+    select = []
+    cancel_delay()
 
     ## Make sure the configuration file is up-to-date.  Don't worry if we
     ## can't do anything useful.
@@ -236,10 +295,14 @@ def kickpeers():
         upness = False
     else:
       addr = None
+    if not T._debug: pass
+    elif addr: print '#   local address = %s' % straddr(addr)
+    else: print '#   offline'
 
     ## Now decide what to do.
     changes = []
     for g, pp in CF.groups:
+      if T._debug: print '#   check group %s' % g
 
       ## Find out which peer in the group ought to be active.
       ip = None
@@ -250,9 +313,14 @@ def kickpeers():
           ipq = addr
         else:
           ipq = localaddr(p)
+        if T._debug:
+          info = 'peer=%s; target=%s; net=%s/%s; local=%s' % (
+            t, p or '(default)', straddr(a), strmask(m), straddr(ipq))
         if upness and ip is None and \
               ipq is not None and (ipq & m) == a:
+          if T._debug: print '#     %s: SELECTED' % info
           map[t] = 'up'
+          select.append('%s=%s' % (g, t))
           if t == 'down' or t.startswith('down/'):
             want = None
           else:
@@ -260,13 +328,16 @@ def kickpeers():
           ip = ipq
         else:
           map[t] = 'down'
+          if T._debug: print '#     %s: skipped' % info
 
       ## Shut down the wrong ones.
       found = False
+      if T._debug: print '#   peer-map = %r' % map
       for p in peers:
         what = map.get(p, 'leave')
         if what == 'up':
           found = True
+          if T._debug: print '#   peer %s: already up' % p
         elif what == 'down':
           def _(p = p):
             try:
@@ -277,41 +348,42 @@ def kickpeers():
                 pass
               else:
                 raise
+          if T._debug: print '#   peer %s: bring down' % p
           changes.append(_)
 
       ## Start the right one if necessary.
-      if want is not None and (not found or ip != lastip.get(g, None)):
+      if want is not None and not found:
         def _(want = want):
           try:
-            SM.svcsubmit('connect', 'active', want)
+            list(SM.svcsubmit('connect', 'active', want))
           except T.TripeError, exc:
             SM.warn('conntrack', 'connect-failed', want, *exc.args)
+        if T._debug: print '#   peer %s: bring up' % want
         changes.append(_)
-      lastip[g] = ip
 
     ## Commit the changes.
     if changes:
-      SM.notify('conntrack', upness and 'up' or 'down', *reason)
+      SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
       for c in changes: c()
 
-def netupdown(upness, reason):
-  """
-  Add or kill peers according to whether the network is up or down.
-
-  UPNESS is true if the network is up, or false if it's down.
-  """
-
-  _kick.put((upness, reason))
-
 ###--------------------------------------------------------------------------
 ### NetworkManager monitor.
 
+DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
+
 NM_NAME = 'org.freedesktop.NetworkManager'
 NM_PATH = '/org/freedesktop/NetworkManager'
 NM_IFACE = NM_NAME
 NMCA_IFACE = NM_NAME + '.Connection.Active'
 
-NM_STATE_CONNECTED = 3
+NM_STATE_CONNECTED = 3 #obsolete
+NM_STATE_CONNECTED_LOCAL = 50
+NM_STATE_CONNECTED_SITE = 60
+NM_STATE_CONNECTED_GLOBAL = 70
+NM_CONNSTATES = set([NM_STATE_CONNECTED,
+                     NM_STATE_CONNECTED_LOCAL,
+                     NM_STATE_CONNECTED_SITE,
+                     NM_STATE_CONNECTED_GLOBAL])
 
 class NetworkManagerMonitor (object):
   """
@@ -330,28 +402,59 @@ class NetworkManagerMonitor (object):
   def attach(me, bus):
     try:
       nm = bus.get_object(NM_NAME, NM_PATH)
-      state = nm.Get(NM_IFACE, 'State')
-      if state == NM_STATE_CONNECTED:
+      state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE)
+      if state in NM_CONNSTATES:
         netupdown(True, ['nm', 'initially-connected'])
       else:
         netupdown(False, ['nm', 'initially-disconnected'])
-    except D.DBusException:
-      pass
-    bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
-                            NM_NAME, NM_PATH)
-    bus.add_signal_receiver(me._nm_connchange,
-                            'PropertiesChanged', NMCA_IFACE,
-                            NM_NAME, None)
+    except D.DBusException, e:
+      if T._debug: print '# exception attaching to network-manager: %s' % e
+    bus.add_signal_receiver(me._nm_state, 'StateChanged',
+                            NM_IFACE, NM_NAME, NM_PATH)
+    bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
+                            NMCA_IFACE, NM_NAME, None)
 
   def _nm_state(me, state):
-    if state == NM_STATE_CONNECTED:
-      netupdown(True, ['nm', 'connected'])
+    if state in NM_CONNSTATES:
+      delay_netupdown(True, ['nm', 'connected'])
     else:
-      netupdown(False, ['nm', 'disconnected'])
+      delay_netupdown(False, ['nm', 'disconnected'])
 
   def _nm_connchange(me, props):
-    if props.get('Default', False):
-      netupdown(True, ['nm', 'default-connection-change'])
+    if props.get('Default', False) or props.get('Default6', False):
+      delay_netupdown(True, ['nm', 'default-connection-change'])
+
+##--------------------------------------------------------------------------
+### Connman monitor.
+
+CM_NAME = 'net.connman'
+CM_PATH = '/'
+CM_IFACE = 'net.connman.Manager'
+
+class ConnManMonitor (object):
+  """
+  Watch ConnMan signls for changes in network state.
+  """
+
+  ## Strategy.  Everything seems to be usefully encoded in the `State'
+  ## property.  If it's `offline', `idle' or `ready' then we don't expect a
+  ## network connection.  During handover from one network to another, the
+  ## property passes through `ready' to `online'.
+
+  def attach(me, bus):
+    try:
+      cm = bus.get_object(CM_NAME, CM_PATH)
+      props = cm.GetProperties(dbus_interface = CM_IFACE)
+      state = props['State']
+      netupdown(state == 'online', ['connman', 'initially-%s' % state])
+    except D.DBusException, e:
+      if T._debug: print '# exception attaching to connman: %s' % e
+    bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
+                            CM_IFACE, CM_NAME, CM_PATH)
+
+  def _cm_state(me, prop, value):
+    if prop != 'State': return
+    delay_netupdown(value == 'online', ['connman', value])
 
 ###--------------------------------------------------------------------------
 ### Maemo monitor.
@@ -383,7 +486,8 @@ class MaemoICdMonitor (object):
       except D.DBusException:
         me._iap = None
         netupdown(False, ['icd', 'initially-disconnected'])
-    except D.DBusException:
+    except D.DBusException, e:
+      if T._debug: print '# exception attaching to icd: %s' % e
       me._iap = None
     bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
                             ICD_NAME, ICD_PATH)
@@ -391,10 +495,10 @@ class MaemoICdMonitor (object):
   def _icd_state(me, iap, ty, state, hunoz):
     if state == 'CONNECTED':
       me._iap = iap
-      netupdown(True, ['icd', 'connected', iap])
+      delay_netupdown(True, ['icd', 'connected', iap])
     elif state == 'IDLE' and iap == me._iap:
       me._iap = None
-      netupdown(False, ['icd', 'idle'])
+      delay_netupdown(False, ['icd', 'idle'])
 
 ###--------------------------------------------------------------------------
 ### D-Bus connection tracking.
@@ -504,9 +608,11 @@ def init():
   T.Coroutine(kickpeers, name = 'kickpeers').switch()
   DBM = DBusMonitor()
   DBM.addmon(NetworkManagerMonitor())
+  DBM.addmon(ConnManMonitor())
   DBM.addmon(MaemoICdMonitor())
-  G.timeout_add_seconds(30, lambda: (netupdown(True, ['interval-timer'])
-                                     or True))
+  G.timeout_add_seconds(30, lambda: (_delay is not None or
+                                     netupdown(True, ['interval-timer']) or
+                                     True))
 
 def parse_options():
   """
@@ -548,7 +654,10 @@ def cmd_updown(upness):
   return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
 service_info = [('conntrack', VERSION, {
   'up': (0, None, '', cmd_updown(True)),
-  'down': (0, None, '', cmd_updown(False))
+  'down': (0, None, '', cmd_updown(False)),
+  'show-config': (0, 0, '', cmd_showconfig),
+  'show-groups': (0, 0, '', cmd_showgroups),
+  'show-group': (1, 1, 'GROUP', cmd_showgroup)
 })]
 
 if __name__ == '__main__':