svc/conntrack.in: Make an `InetAddress' class to do address wrangling.
[tripe] / svc / conntrack.in
index 3368295..83b2993 100644 (file)
@@ -95,6 +95,60 @@ def toposort(cmp, things):
       break
 
 ###--------------------------------------------------------------------------
+### Address manipulation.
+
+class InetAddress (object):
+  def __init__(me, addrstr, maskstr = None):
+    me.addr = me._addrstr_to_int(addrstr)
+    if maskstr is None:
+      me.mask = -1
+    elif maskstr.isdigit():
+      me.mask = (1 << 32) - (1 << 32 - int(maskstr))
+    else:
+      me.mask = me._addrstr_to_int(maskstr)
+    if me.addr&~me.mask:
+      raise ValueError('network contains bits set beyond mask')
+  def _addrstr_to_int(me, addrstr):
+    return unpack('>L', S.inet_aton(addrstr))[0]
+  def _int_to_addrstr(me, n):
+    return S.inet_ntoa(pack('>L', n))
+  def sockaddr(me, port = 0):
+    if me.mask != -1: raise ValueError('not a simple address')
+    return me._int_to_addrstr(me.addr), port
+  def __str__(me):
+    addrstr = me._int_to_addrstr(me.addr)
+    if me.mask == -1:
+      return addrstr
+    else:
+      inv = me.mask ^ ((1 << 32) - 1)
+      if (inv&(inv + 1)) == 0:
+        return '%s/%d' % (addrstr, 32 - inv.bit_length())
+      else:
+        return '%s/%s' % (addrstr, me._int_to_addrstr(me.mask))
+  def withinp(me, net):
+    if (me.mask&net.mask) != net.mask: return False
+    if (me.addr ^ net.addr)&net.mask: return False
+    return True
+  def eq(me, other):
+    if me.mask != other.mask: return False
+    if me.addr != other.addr: return False
+    return True
+  @classmethod
+  def from_sockaddr(cls, sa):
+    addr, port = (lambda a, p: (a, p))(*sa)
+    return cls(addr), port
+
+def parse_address(addrstr, maskstr = None):
+  return InetAddress(addrstr, maskstr)
+
+def parse_net(netstr):
+  try: sl = netstr.index('/')
+  except ValueError: raise ValueError('missing mask')
+  return parse_address(netstr[:sl], netstr[sl + 1:])
+
+def straddr(a): return a is None and '#<none>' or str(a)
+
+###--------------------------------------------------------------------------
 ### Parse the configuration file.
 
 ## Hmm.  Should I try to integrate this with the peers database?  It's not a
@@ -102,18 +156,19 @@ def toposort(cmp, things):
 ## this service are largely going to be satellite notes, I don't think
 ## scalability's going to be a problem.
 
+TESTADDR = InetAddress('1.2.3.4')
+
 class Config (object):
   """
   Represents a configuration file.
 
   The most interesting thing is probably the `groups' slot, which stores a
   list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
-  list of (TAG, PEER, ADDR, MASK) triples.  The implication is that there
-  should be precisely one peer with a name matching NAME-*, and that it
-  should be NAME-TAG, where (TAG, PEER, ADDR, MASK) is the first triple such
-  that the host's primary IP address (if PEER is None -- or the IP address it
-  would use for communicating with PEER) is within the network defined by
-  ADDR/MASK.
+  list of (TAG, PEER, NET) triples.  The implication is that there should be
+  precisely one peer from the set, and that it should be named TAG, where
+  (TAG, PEER, NET) is the first triple such that the host's primary IP
+  address (if PEER is None -- or the IP address it would use for
+  communicating with PEER) is within the NET.
   """
 
   def __init__(me, file):
@@ -147,10 +202,9 @@ class Config (object):
     ## actually in use.  Note that we never send packets to the test address;
     ## we just use it to discover routing information.
     if cp.has_option('DEFAULT', 'test-addr'):
-      testaddr = cp.get('DEFAULT', 'test-addr')
-      S.inet_aton(testaddr)
+      testaddr = InetAddress(cp.get('DEFAULT', 'test-addr'))
     else:
-      testaddr = '1.2.3.4'
+      testaddr = TESTADDR
 
     ## Scan the configuration file and build the groups structure.
     groups = []
@@ -164,26 +218,21 @@ class Config (object):
           peer = None
           net = spec[0]
         else:
-          peer, net = spec
+          peer = InetAddress(spec[0])
+          net = spec[1]
 
         ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
         ## 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:
-          n = int(net[slash + 1:], 10)
-          mask = (1 << 32) - (1 << 32 - n)
-        pats.append((tag, peer, addr & mask, mask))
+        net = parse_net(net)
+        pats.append((tag, peer, net))
 
       ## Annoyingly, RawConfigParser doesn't preserve the order of options.
       ## In order to make things vaguely sane, we topologically sort the
       ## patterns so that more specific patterns are checked first.
-      pats = list(toposort(lambda (t, p, a, m), (tt, pp, aa, mm): \
+      pats = list(toposort(lambda (t, p, n), (tt, pp, nn): \
                              (p and not pp) or \
-                             (p == pp and m == (m | mm) and aa == (a & mm)),
+                             (p == pp and n.withinp(nn)),
                            pats))
       groups.append((sec, pats))
 
@@ -194,12 +243,6 @@ 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 i
-  return straddr(m)
-
 def cmd_showconfig():
   T.svcinfo('test-addr=%s' % CF.testaddr)
 def cmd_showgroups():
@@ -212,10 +255,10 @@ def cmd_showgroup(g):
       break
   else:
     raise T.TripeJobError('unknown-group', g)
-  for t, p, a, m in pats:
+  for t, p, n in pats:
     T.svcinfo('peer', t,
-              'target', p or '(default)',
-              'net', '%s/%s' % (straddr(a), strmask(m)))
+              'target', p and str(p) or '(default)',
+              'net', str(n))
 
 ###--------------------------------------------------------------------------
 ### Responding to a network up/down event.
@@ -227,21 +270,51 @@ def localaddr(peer):
   sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
   try:
     try:
-      sk.connect((peer, 1))
-      addr, _ = sk.getsockname()
-      addr, = unpack('>L', S.inet_aton(addr))
-      return addr
+      sk.connect(peer.sockaddr(1))
+      addr = sk.getsockname()
+      return InetAddress.from_sockaddr(addr)[0]
     except S.error:
       return None
   finally:
     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():
   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.
@@ -274,16 +347,16 @@ def kickpeers():
       ip = None
       map = {}
       want = None
-      for t, p, a, m in pp:
+      for t, p, n in pp:
         if p is None or not upness:
           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))
+          info = 'peer=%s; target=%s; net=%s; local=%s' % (
+            t, p or '(default)', n, straddr(ipq))
         if upness and ip is None and \
-              ipq is not None and (ipq & m) == a:
+              ipq is not None and ipq.withinp(n):
           if T._debug: print '#     %s: SELECTED' % info
           map[t] = 'up'
           select.append('%s=%s' % (g, t))
@@ -332,15 +405,6 @@ def kickpeers():
       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.
 
@@ -391,13 +455,13 @@ class NetworkManagerMonitor (object):
 
   def _nm_state(me, state):
     if state in NM_CONNSTATES:
-      netupdown(True, ['nm', 'connected'])
+      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.
@@ -429,7 +493,7 @@ class ConnManMonitor (object):
 
   def _cm_state(me, prop, value):
     if prop != 'State': return
-    netupdown(value == 'online', ['connman', value])
+    delay_netupdown(value == 'online', ['connman', value])
 
 ###--------------------------------------------------------------------------
 ### Maemo monitor.
@@ -470,10 +534,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.
@@ -585,8 +649,9 @@ def init():
   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():
   """