svc/conntrack.in: Process peer patterns in order.
authorMark Wooding <mdw@distorted.org.uk>
Fri, 29 Sep 2017 00:05:26 +0000 (01:05 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 28 Jun 2018 23:29:24 +0000 (00:29 +0100)
Rewrite the configuration file parser entirely so as to process the
patterns in order, rather than messing about with topological sorting.
This will let us introduce various improvements to patterns which don't
have a clear specificness ordering.

svc/conntrack.8.in
svc/conntrack.in

index 713a70b..004ffa8 100644 (file)
@@ -127,22 +127,8 @@ particular
 .I remote-addr
 to use when checking whether a particular peer is applicable.
 .PP
-The peer definitions can be in any order.  They are checked
-most-specific first, and searching stops as soon as a match is found.
-Therefore a default definition can be added as
-.IP
-.I tag
-.B =
-.B 0/0
-.PP
-without fear of overriding any more specific definitions.  For avoidance
-of doubt, one peer definition is
-.I more specific
-than another if either the former has a specified
-.I remote-addr
-and the latter has not, or the former is wholly contained within the
-latter.  (Overlapping definitions are not recommended, and will be
-processed in an arbitrary order.)
+The peer definitions in each group are checked in the order given, and
+searching stops as soon as a match is found.
 .PP
 Peers are connected using the
 .BR connect (8)
index b840fc2..043c969 100644 (file)
@@ -36,6 +36,7 @@ import socket as S
 import mLib as M
 import tripe as T
 import dbus as D
+import re as RX
 for i in ['mainloop', 'mainloop.glib']:
   __import__('dbus.%s' % i)
 try: from gi.repository import GLib as G
@@ -53,47 +54,6 @@ class struct (object):
   def __init__(me, **kw):
     me.__dict__.update(kw)
 
-def toposort(cmp, things):
-  """
-  Generate the THINGS in an order consistent with a given partial order.
-
-  The function CMP(X, Y) should return true if X must precede Y, and false if
-  it doesn't care.  If X and Y are equal then it should return false.
-
-  The THINGS may be any finite iterable; it is converted to a list
-  internally.
-  """
-
-  ## Make sure we can index the THINGS, and prepare an ordering table.
-  ## What's going on?  The THINGS might not have a helpful equality
-  ## predicate, so it's easier to work with indices.  The ordering table will
-  ## remember which THINGS (by index) are considered greater than other
-  ## things.
-  things = list(things)
-  n = len(things)
-  order = [{} for i in xrange(n)]
-  rorder = [{} for i in xrange(n)]
-  for i in xrange(n):
-    for j in xrange(n):
-      if i != j and cmp(things[i], things[j]):
-        order[j][i] = True
-        rorder[i][j] = True
-
-  ## Now we can do the sort.
-  out = []
-  while True:
-    done = True
-    for i in xrange(n):
-      if order[i] is not None:
-        done = False
-        if len(order[i]) == 0:
-          for j in rorder[i]:
-            del order[j][i]
-          yield things[i]
-          order[i] = None
-    if done:
-      break
-
 ###--------------------------------------------------------------------------
 ### Address manipulation.
 
@@ -158,6 +118,19 @@ def straddr(a): return a is None and '#<none>' or str(a)
 
 TESTADDR = InetAddress('1.2.3.4')
 
+CONFSYNTAX = [
+  ('COMMENT', RX.compile(r'^\s*($|[;#])')),
+  ('GRPHDR', RX.compile(r'^\s*\[(.*)\]\s*$')),
+  ('ASSGN', RX.compile(r'\s*([\w.-]+)\s*[:=]\s*(|\S|\S.*\S)\s*$'))]
+
+class ConfigError (Exception):
+  def __init__(me, file, lno, msg):
+    me.file = file
+    me.lno = lno
+    me.msg = msg
+  def __str__(me):
+    return '%s:%d: %s' % (me.file, me.lno, me.msg)
+
 class Config (object):
   """
   Represents a configuration file.
@@ -191,52 +164,99 @@ class Config (object):
     Internal function to update the configuration from the underlying file.
     """
 
-    ## Read the configuration.  We have no need of the fancy substitutions,
-    ## 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
-    ## 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 = InetAddress(cp.get('DEFAULT', 'test-addr'))
-    else:
-      testaddr = TESTADDR
-
-    ## Scan the configuration file and build the groups structure.
+    ## Initial state.
+    testaddr = None
     groups = {}
-    for sec in cp.sections():
-      pats = []
-      for tag in cp.options(sec):
-        spec = cp.get(sec, tag).split()
-
-        ## Parse the entry into peer and network.
-        if len(spec) == 1:
-          peer = None
-          net = spec[0]
+    grpname = None
+    grplist = []
+
+    ## Open the file and start reading.
+    with open(me._file) as f:
+      lno = 0
+      for line in f:
+        lno += 1
+        for tag, rx in CONFSYNTAX:
+          m = rx.match(line)
+          if m: break
         else:
-          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.
-        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, n), (tt, pp, nn): \
-                             (p and not pp) or \
-                             (p == pp and n.withinp(nn)),
-                           pats))
-      groups[sec] = pats
+          raise ConfigError(me._file, lno, 'failed to parse line: %r' % line)
+
+        if tag == 'COMMENT':
+          ## A comment.  Ignore it and hope it goes away.
+
+          continue
+
+        elif tag == 'GRPHDR':
+          ## A group header.  Flush the old group and start a new one.
+          newname = m.group(1)
+
+          if grpname is not None: groups[grpname] = grplist
+          if newname in groups:
+            raise ConfigError(me._file, lno,
+                              "duplicate group name `%s'" % newname)
+          grpname = newname
+          grplist = []
+
+        elif tag == 'ASSGN':
+           ## An assignment.  Deal with it.
+          name, value = m.group(1), m.group(2)
+
+          if grpname is None:
+            ## We're outside of any group, so this is a global configuration
+            ## tweak.
+
+            if name == 'test-addr':
+              for astr in value.split():
+                try:
+                  a = parse_address(astr)
+                except Exception, e:
+                  raise ConfigError(me._file, lno,
+                                    "invalid IP address `%s': %s" %
+                                    (astr, e))
+                if testaddr is not None:
+                  raise ConfigError(me._file, lno, 'duplicate test-address')
+                testaddr = a
+            else:
+              raise ConfigError(me._file, lno,
+                                "unknown global option `%s'" % name)
+
+          else:
+            ## Parse a pattern and add it to the group.
+            spec = value.split()
+            i = 0
+
+            ## Check for an explicit target address.
+            if i >= len(spec) or spec[i].find('/') >= 0:
+              peer = None
+            else:
+              try:
+                peer = parse_address(spec[i])
+              except Exception, e:
+                raise ConfigError(me._file, lno,
+                                  "invalid IP address `%s': %s" %
+                                  (spec[i], e))
+              i += 1
+
+            ## Parse the local network.
+            if len(spec) != i + 1:
+              raise ConfigError(me._file, lno, 'no network defined')
+            try:
+              net = parse_net(spec[i])
+            except Exception, e:
+              raise ConfigError(me._file, lno,
+                                "invalid IP network `%s': %s" %
+                                (spec[i], e))
+
+            ## Add this entry to the list.
+            grplist.append((name, peer, net))
+
+    ## Fill in the default test address if necessary.
+    if testaddr is None: testaddr = TESTADDR
 
     ## Done.
+    if grpname is not None: groups[grpname] = grplist
     me.testaddr = testaddr
     me.groups = groups