chiark / gitweb /
make-secnet-sites: Tainted.name: take `what'
[secnet.git] / make-secnet-sites
1 #! /usr/bin/env python3
2 #
3 # This file is part of secnet.
4 # See README for full list of copyright holders.
5 #
6 # secnet is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10
11 # secnet is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # version 3 along with secnet; if not, see
18 # https://www.gnu.org/licenses/gpl.html.
19
20 """VPN sites file manipulation.
21
22 This program enables VPN site descriptions to be submitted for
23 inclusion in a central database, and allows the resulting database to
24 be turned into a secnet configuration file.
25
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
28
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
31
32 The program expects to be invoked via userv to manage the database; it
33 relies on the USERV_USER and USERV_GROUP environment variables. The
34 command line arguments for this invocation are:
35
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37   group
38
39 All but the last argument are expected to be set by userv; the 'group'
40 argument is provided by the user. A suitable userv configuration file
41 fragment is:
42
43 reset
44 no-disconnect-hup
45 no-suppress-args
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
48
49 This program is part of secnet.
50
51 """
52
53 from __future__ import print_function
54 from __future__ import unicode_literals
55 from builtins import int
56
57 import string
58 import time
59 import sys
60 import os
61 import getopt
62 import re
63 import argparse
64 import math
65
66 import ipaddress
67
68 # entry 0 is "near the executable", or maybe from PYTHONPATH=.,
69 # which we don't want to preempt
70 sys.path.insert(1,"/usr/local/share/secnet")
71 sys.path.insert(1,"/usr/share/secnet")
72 import ipaddrset
73
74 from argparseactionnoyes import ActionNoYes
75
76 VERSION="0.1.18"
77
78 from sys import version_info
79 if version_info.major == 2:  # for python2
80     import codecs
81     sys.stdin = codecs.getreader('utf-8')(sys.stdin)
82     sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
83     import io
84     open=lambda f,m='r': io.open(f,m,encoding='utf-8')
85
86 max={'rsa_bits':8200,'name':33,'dh_bits':8200}
87
88 class Tainted:
89         def __init__(self,s,tline=None,tfile=None):
90                 self._s=s
91                 self._ok=None
92                 self._line=line if tline is None else tline
93                 self._file=file if tfile is None else tfile
94         def __eq__(self,e):
95                 return self._s==e
96         def __ne__(self,e):
97                 # for Python2
98                 return not self.__eq__(e)
99         def __str__(self):
100                 raise RuntimeError('direct use of Tainted value')
101         def __repr__(self):
102                 return 'Tainted(%s)' % repr(self._s)
103
104         def _bad(self,what,why):
105                 assert(self._ok is not True)
106                 self._ok=False
107                 complain('bad parameter: %s: %s' % (what, why))
108                 return False
109
110         def _max_ok(self,what,maxlen):
111                 if len(self._s) > maxlen:
112                         return self._bad(what,'too long (max %d)' % maxlen)
113                 return True
114
115         def _re_ok(self,bad,what,maxlen=None):
116                 if maxlen is None: maxlen=max[what]
117                 self._max_ok(what,maxlen)
118                 if self._ok is False: return False
119                 if bad.search(self._s): return self._bad(what,'bad syntax')
120                 return True
121
122         def _rtnval(self, is_ok, ifgood, ifbad=''):
123                 if is_ok:
124                         assert(self._ok is not False)
125                         self._ok=True
126                         return ifgood
127                 else:
128                         assert(self._ok is not True)
129                         self._ok=False
130                         return ifbad
131
132         def _rtn(self, is_ok, ifbad=''):
133                 return self._rtnval(is_ok, self._s, ifbad)
134
135         def raw(self):
136                 return self._s
137         def raw_mark_ok(self):
138                 # caller promises to throw if syntax was dangeorus
139                 return self._rtn(True)
140
141         def output(self):
142                 if self._ok is False: return ''
143                 if self._ok is True: return self._s
144                 print('%s:%d: unchecked/unknown additional data "%s"' %
145                       (self._file,self._line,self._s),
146                       file=sys.stderr)
147                 sys.exit(1)
148
149         bad_name=re.compile(r'^[^a-zA-Z]|[^-_0-9a-zA-Z]')
150         # secnet accepts _ at start of names, but we reserve that
151         bad_name_counter=0
152         def name(self,what='name'):
153                 ok=self._re_ok(Tainted.bad_name,what)
154                 return self._rtn(ok,
155                                  '_line%d_%s' % (self._line, id(self)))
156
157         def keyword(self):
158                 ok=self._s in keywords or self._s in levels
159                 if not ok:
160                         complain('unknown keyword %s' % self._s)
161                 return self._rtn(ok)
162
163         bad_hex=re.compile(r'[^0-9a-fA-F]')
164         def bignum_16(self,kind,what):
165                 maxlen=(max[kind+'_bits']+3)/4
166                 ok=self._re_ok(Tainted.bad_hex,what,maxlen)
167                 return self._rtn(ok)
168
169         bad_num=re.compile(r'[^0-9]')
170         def bignum_10(self,kind,what):
171                 maxlen=math.ceil(max[kind+'_bits'] / math.log10(2))
172                 ok=self._re_ok(Tainted.bad_num,what,maxlen)
173                 return self._rtn(ok)
174
175         def number(self,minn,maxx,what='number'):
176                 # not for bignums
177                 ok=self._re_ok(Tainted.bad_num,what,10)
178                 if ok:
179                         v=int(self._s)
180                         if v<minn or v>maxx:
181                                 ok=self._bad(what,'out of range %d..%d'
182                                              % (minn,maxx))
183                 return self._rtnval(ok,v,minn)
184
185         bad_host=re.compile(r'[^-\][_.:0-9a-zA-Z]')
186         # We permit _ so we can refer to special non-host domains
187         # which have A and AAAA RRs.  This is a crude check and we may
188         # still produce config files with syntactically invalid
189         # domains or addresses, but that is OK.
190         def host(self):
191                 ok=self._re_ok(Tainted.bad_host,'host/address',255)
192                 return self._rtn(ok)
193
194         bad_email=re.compile(r'[^-._0-9a-z@!$%^&*=+~/]')
195         # ^ This does not accept all valid email addresses.  That's
196         # not really possible with this input syntax.  It accepts
197         # all ones that don't require quoting anywhere in email
198         # protocols (and also accepts some invalid ones).
199         def email(self):
200                 ok=self._re_ok(Tainted.bad_email,'email address',1023)
201                 return self._rtn(ok)
202
203         bad_groupname=re.compile(r'^[^_A-Za-z]|[^-+_0-9A-Za-z]')
204         def groupname(self):
205                 ok=self._re_ok(Tainted.bad_groupname,'group name',64)
206                 return self._rtn(ok)
207
208 def parse_args():
209         global service
210         global inputfile
211         global header
212         global groupfiledir
213         global sitesfile
214         global outputfile
215         global group
216         global user
217         global of
218         global prefix
219         global key_prefix
220
221         ap = argparse.ArgumentParser(description='process secnet sites files')
222         ap.add_argument('--userv', '-u', action='store_true',
223                         help='userv service fragment update mode')
224         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
225                         default=True,
226                  help='prefix conf file key names derived from sites data')
227         ap.add_argument('--prefix', '-P', nargs=1,
228                         help='set prefix')
229         ap.add_argument('arg',nargs=argparse.REMAINDER)
230         av = ap.parse_args()
231         #print(repr(av), file=sys.stderr)
232         service = 1 if av.userv else 0
233         prefix = '' if av.prefix is None else av.prefix[0]
234         key_prefix = av.conf_key_prefix
235         if service:
236                 if len(av.arg)!=4:
237                         print("Wrong number of arguments")
238                         sys.exit(1)
239                 (header, groupfiledir, sitesfile, group) = av.arg
240                 group = Tainted(group,0,'command line')
241                 # untrusted argument from caller
242                 if "USERV_USER" not in os.environ:
243                         print("Environment variable USERV_USER not found")
244                         sys.exit(1)
245                 user=os.environ["USERV_USER"]
246                 # Check that group is in USERV_GROUP
247                 if "USERV_GROUP" not in os.environ:
248                         print("Environment variable USERV_GROUP not found")
249                         sys.exit(1)
250                 ugs=os.environ["USERV_GROUP"]
251                 ok=0
252                 for i in ugs.split():
253                         if group==i: ok=1
254                 if not ok:
255                         print("caller not in group %s"%group)
256                         sys.exit(1)
257         else:
258                 if len(av.arg)>3:
259                         print("Too many arguments")
260                         sys.exit(1)
261                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
262
263 parse_args()
264
265 # Classes describing possible datatypes in the configuration file
266
267 class basetype:
268         "Common protocol for configuration types."
269         def add(self,obj,w):
270                 complain("%s %s already has property %s defined"%
271                         (obj.type,obj.name,w[0].raw()))
272
273 class conflist:
274         "A list of some kind of configuration type."
275         def __init__(self,subtype,w):
276                 self.subtype=subtype
277                 self.list=[subtype(w)]
278         def add(self,obj,w):
279                 self.list.append(self.subtype(w))
280         def __str__(self):
281                 return ', '.join(map(str, self.list))
282 def listof(subtype):
283         return lambda w: conflist(subtype, w)
284
285 class single_ipaddr (basetype):
286         "An IP address"
287         def __init__(self,w):
288                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
289         def __str__(self):
290                 return '"%s"'%self.addr
291
292 class networks (basetype):
293         "A set of IP addresses specified as a list of networks"
294         def __init__(self,w):
295                 self.set=ipaddrset.IPAddressSet()
296                 for i in w[1:]:
297                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
298                         self.set.append([x])
299         def __str__(self):
300                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
301
302 class dhgroup (basetype):
303         "A Diffie-Hellman group"
304         def __init__(self,w):
305                 self.mod=w[1].bignum_16('dh','dh mod')
306                 self.gen=w[2].bignum_16('dh','dh gen')
307         def __str__(self):
308                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
309
310 class hash (basetype):
311         "A choice of hash function"
312         def __init__(self,w):
313                 hname=w[1]
314                 self.ht=hname.raw()
315                 if (self.ht!='md5' and self.ht!='sha1'):
316                         complain("unknown hash type %s"%(self.ht))
317                         self.ht=None
318                 else:
319                         hname.raw_mark_ok()
320         def __str__(self):
321                 return '%s'%(self.ht)
322
323 class email (basetype):
324         "An email address"
325         def __init__(self,w):
326                 self.addr=w[1].email()
327         def __str__(self):
328                 return '<%s>'%(self.addr)
329
330 class boolean (basetype):
331         "A boolean"
332         def __init__(self,w):
333                 v=w[1]
334                 if re.match('[TtYy1]',v.raw()):
335                         self.b=True
336                         v.raw_mark_ok()
337                 elif re.match('[FfNn0]',v.raw()):
338                         self.b=False
339                         v.raw_mark_ok()
340                 else:
341                         complain("invalid boolean value");
342         def __str__(self):
343                 return ['False','True'][self.b]
344
345 class num (basetype):
346         "A decimal number"
347         def __init__(self,w):
348                 self.n=w[1].number(0,0x7fffffff)
349         def __str__(self):
350                 return '%d'%(self.n)
351
352 class address (basetype):
353         "A DNS name and UDP port number"
354         def __init__(self,w):
355                 self.adr=w[1].host()
356                 self.port=w[2].number(1,65536,'port')
357         def __str__(self):
358                 return '"%s"; port %d'%(self.adr,self.port)
359
360 class rsakey (basetype):
361         "An RSA public key"
362         def __init__(self,w):
363                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
364                 self.e=w[2].bignum_10('rsa','rsa e')
365                 self.n=w[3].bignum_10('rsa','rsa n')
366                 if len(w) >= 5: w[4].email()
367         def __str__(self):
368                 return 'rsa-public("%s","%s")'%(self.e,self.n)
369
370 # Possible properties of configuration nodes
371 keywords={
372  'contact':(email,"Contact address"),
373  'dh':(dhgroup,"Diffie-Hellman group"),
374  'hash':(hash,"Hash function"),
375  'key-lifetime':(num,"Maximum key lifetime (ms)"),
376  'setup-timeout':(num,"Key setup timeout (ms)"),
377  'setup-retries':(num,"Maximum key setup packet retries"),
378  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
379  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
380  'restrict-nets':(networks,"Allowable networks"),
381  'networks':(networks,"Claimed networks"),
382  'pubkey':(rsakey,"RSA public site key"),
383  'peer':(single_ipaddr,"Tunnel peer IP address"),
384  'address':(address,"External contact address and port"),
385  'mobile':(boolean,"Site is mobile"),
386 }
387
388 def sp(name,value):
389         "Simply output a property - the default case"
390         return "%s %s;\n"%(name,value)
391
392 # All levels support these properties
393 global_properties={
394         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
395         'dh':sp,
396         'hash':sp,
397         'key-lifetime':sp,
398         'setup-timeout':sp,
399         'setup-retries':sp,
400         'wait-time':sp,
401         'renegotiate-time':sp,
402         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
403 }
404
405 class level:
406         "A level in the configuration hierarchy"
407         depth=0
408         leaf=0
409         allow_properties={}
410         require_properties={}
411         def __init__(self,w):
412                 self.type=w[0].keyword()
413                 self.name=w[1].name()
414                 self.properties={}
415                 self.children={}
416         def indent(self,w,t):
417                 w.write("                 "[:t])
418         def prop_out(self,n):
419                 return self.allow_properties[n](n,str(self.properties[n]))
420         def output_props(self,w,ind):
421                 for i in sorted(self.properties.keys()):
422                         if self.allow_properties[i]:
423                                 self.indent(w,ind)
424                                 w.write("%s"%self.prop_out(i))
425         def kname(self):
426                 return ((self.type[0].upper() if key_prefix else '')
427                         + self.name)
428         def output_data(self,w,path):
429                 ind = 2*len(path)
430                 self.indent(w,ind)
431                 w.write("%s {\n"%(self.kname()))
432                 self.output_props(w,ind+2)
433                 if self.depth==1: w.write("\n");
434                 for k in sorted(self.children.keys()):
435                         c=self.children[k]
436                         c.output_data(w,path+(c,))
437                 self.indent(w,ind)
438                 w.write("};\n")
439
440 class vpnlevel(level):
441         "VPN level in the configuration hierarchy"
442         depth=1
443         leaf=0
444         type="vpn"
445         allow_properties=global_properties.copy()
446         require_properties={
447          'contact':"VPN admin contact address"
448         }
449         def __init__(self,w):
450                 level.__init__(self,w)
451         def output_vpnflat(self,w,path):
452                 "Output flattened list of site names for this VPN"
453                 ind=2*(len(path)+1)
454                 self.indent(w,ind)
455                 w.write("%s {\n"%(self.kname()))
456                 for i in self.children.keys():
457                         self.children[i].output_vpnflat(w,path+(self,))
458                 w.write("\n")
459                 self.indent(w,ind+2)
460                 w.write("all-sites %s;\n"%
461                         ','.join(map(lambda i: i.kname(),
462                                      self.children.values())))
463                 self.indent(w,ind)
464                 w.write("};\n")
465
466 class locationlevel(level):
467         "Location level in the configuration hierarchy"
468         depth=2
469         leaf=0
470         type="location"
471         allow_properties=global_properties.copy()
472         require_properties={
473          'contact':"Location admin contact address",
474         }
475         def __init__(self,w):
476                 level.__init__(self,w)
477                 self.group=w[2].groupname()
478         def output_vpnflat(self,w,path):
479                 ind=2*(len(path)+1)
480                 self.indent(w,ind)
481                 # The "path=path,self=self" abomination below exists because
482                 # Python didn't support nested_scopes until version 2.1
483                 #
484                 #"/"+self.name+"/"+i
485                 w.write("%s %s;\n"%(self.kname(),','.join(
486                         map(lambda x,path=path,self=self:
487                             '/'.join([prefix+"vpn-data"] + list(map(
488                                     lambda i: i.kname(),
489                                     path+(self,x)))),
490                             self.children.values()))))
491
492 class sitelevel(level):
493         "Site level (i.e. a leafnode) in the configuration hierarchy"
494         depth=3
495         leaf=1
496         type="site"
497         allow_properties=global_properties.copy()
498         allow_properties.update({
499          'address':sp,
500          'networks':None,
501          'peer':None,
502          'pubkey':(lambda n,v:"key %s;\n"%v),
503          'mobile':sp,
504         })
505         require_properties={
506          'dh':"Diffie-Hellman group",
507          'contact':"Site admin contact address",
508          'networks':"Networks claimed by the site",
509          'hash':"hash function",
510          'peer':"Gateway address of the site",
511          'pubkey':"RSA public key of the site",
512         }
513         def __init__(self,w):
514                 level.__init__(self,w)
515         def output_data(self,w,path):
516                 ind=2*len(path)
517                 np='/'.join(map(lambda i: i.name, path))
518                 self.indent(w,ind)
519                 w.write("%s {\n"%(self.kname()))
520                 self.indent(w,ind+2)
521                 w.write("name \"%s\";\n"%(np,))
522                 self.output_props(w,ind+2)
523                 self.indent(w,ind+2)
524                 w.write("link netlink {\n");
525                 self.indent(w,ind+4)
526                 w.write("routes %s;\n"%str(self.properties["networks"]))
527                 self.indent(w,ind+4)
528                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
529                 self.indent(w,ind+2)
530                 w.write("};\n")
531                 self.indent(w,ind)
532                 w.write("};\n")
533
534 # Levels in the configuration file
535 # (depth,properties)
536 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
537
538 def complain(msg):
539         "Complain about a particular input line"
540         moan(("%s line %d: "%(file,line))+msg)
541 def moan(msg):
542         "Complain about something in general"
543         global complaints
544         print(msg);
545         if complaints is None: sys.exit(1)
546         complaints=complaints+1
547
548 class UntaintedRoot():
549         def __init__(self,s): self._s=s
550         def name(self): return self._s
551         def keyword(self): return self._s
552
553 root=level([UntaintedRoot(x) for x in ['root','root']])
554 # All vpns are children of this node
555 obstack=[root]
556 allow_defs=0   # Level above which new definitions are permitted
557
558 def set_property(obj,w):
559         "Set a property on a configuration node"
560         prop=w[0]
561         if prop.raw() in obj.properties:
562                 obj.properties[prop.raw_mark_ok()].add(obj,w)
563         else:
564                 obj.properties[prop.raw()]=keywords[prop.raw_mark_ok()][0](w)
565
566
567 def pline(il,allow_include=False):
568         "Process a configuration file line"
569         global allow_defs, obstack, root
570         w=il.rstrip('\n').split()
571         if len(w)==0: return ['']
572         w=list([Tainted(x) for x in w])
573         keyword=w[0]
574         current=obstack[len(obstack)-1]
575         copyout=lambda: ['    '*len(obstack) +
576                         ' '.join([ww.output() for ww in w]) +
577                         '\n']
578         if keyword=='end-definitions':
579                 keyword.raw_mark_ok()
580                 allow_defs=sitelevel.depth
581                 obstack=[root]
582                 return copyout()
583         if keyword=='include':
584                 if not allow_include:
585                         complain("include not permitted here")
586                         return []
587                 if len(w) != 2:
588                         complain("include requires one argument")
589                         return []
590                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
591                 # ^ user of "include" is trusted so raw_mark_ok is good
592                 return pfilepath(newfile,allow_include=allow_include)
593         if keyword.raw() in levels:
594                 # We may go up any number of levels, but only down by one
595                 newdepth=levels[keyword.raw_mark_ok()].depth
596                 currentdepth=len(obstack) # actually +1...
597                 if newdepth<=currentdepth:
598                         obstack=obstack[:newdepth]
599                 if newdepth>currentdepth:
600                         complain("May not go from level %d to level %d"%
601                                 (currentdepth-1,newdepth))
602                 # See if it's a new one (and whether that's permitted)
603                 # or an existing one
604                 current=obstack[len(obstack)-1]
605                 tname=w[1].name()
606                 if tname in current.children:
607                         # Not new
608                         current=current.children[tname]
609                         if service and group and current.depth==2:
610                                 if group!=current.group:
611                                         complain("Incorrect group!")
612                                 w[2].groupname()
613                 else:
614                         # New
615                         # Ignore depth check for now
616                         nl=levels[keyword.raw()](w)
617                         if nl.depth<allow_defs:
618                                 complain("New definitions not allowed at "
619                                         "level %d"%nl.depth)
620                                 # we risk crashing if we continue
621                                 sys.exit(1)
622                         current.children[tname]=nl
623                         current=nl
624                 obstack.append(current)
625                 return copyout()
626         if keyword.raw() not in current.allow_properties:
627                 complain("Property %s not allowed at %s level"%
628                         (keyword.raw(),current.type))
629                 return []
630         elif current.depth == vpnlevel.depth < allow_defs:
631                 complain("Not allowed to set VPN properties here")
632                 return []
633         else:
634                 set_property(current,w)
635                 return copyout()
636
637         complain("unknown keyword '%s'"%(keyword.raw()))
638
639 def pfilepath(pathname,allow_include=False):
640         f=open(pathname)
641         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
642         f.close()
643         return outlines
644
645 def pfile(name,lines,allow_include=False):
646         "Process a file"
647         global file,line
648         file=name
649         line=0
650         outlines=[]
651         for i in lines:
652                 line=line+1
653                 if (i[0]=='#'): continue
654                 outlines += pline(i,allow_include=allow_include)
655         return outlines
656
657 def outputsites(w):
658         "Output include file for secnet configuration"
659         w.write("# secnet sites file autogenerated by make-secnet-sites "
660                 +"version %s\n"%VERSION)
661         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
662         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
663
664         # Raw VPN data section of file
665         w.write(prefix+"vpn-data {\n")
666         for i in root.children.values():
667                 i.output_data(w,(i,))
668         w.write("};\n")
669
670         # Per-VPN flattened lists
671         w.write(prefix+"vpn {\n")
672         for i in root.children.values():
673                 i.output_vpnflat(w,())
674         w.write("};\n")
675
676         # Flattened list of sites
677         w.write(prefix+"all-sites %s;\n"%",".join(
678                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
679                         root.children.values())))
680
681 line=0
682 file=None
683 complaints=0
684
685 # Sanity check section
686 # Delete nodes where leaf=0 that have no children
687
688 def live(n):
689         "Number of leafnodes below node n"
690         if n.leaf: return 1
691         for i in n.children.keys():
692                 if live(n.children[i]): return 1
693         return 0
694 def delempty(n):
695         "Delete nodes that have no leafnode children"
696         for i in list(n.children.keys()):
697                 delempty(n.children[i])
698                 if not live(n.children[i]):
699                         del n.children[i]
700
701 # Check that all constraints are met (as far as I can tell
702 # restrict-nets/networks/peer are the only special cases)
703
704 def checkconstraints(n,p,ra):
705         new_p=p.copy()
706         new_p.update(n.properties)
707         for i in n.require_properties.keys():
708                 if i not in new_p:
709                         moan("%s %s is missing property %s"%
710                                 (n.type,n.name,i))
711         for i in new_p.keys():
712                 if i not in n.allow_properties:
713                         moan("%s %s has forbidden property %s"%
714                                 (n.type,n.name,i))
715         # Check address range restrictions
716         if "restrict-nets" in n.properties:
717                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
718         else:
719                 new_ra=ra
720         if "networks" in n.properties:
721                 if not n.properties["networks"].set <= new_ra:
722                         moan("%s %s networks out of bounds"%(n.type,n.name))
723                 if "peer" in n.properties:
724                         if not n.properties["networks"].set.contains(
725                                 n.properties["peer"].addr):
726                                 moan("%s %s peer not in networks"%(n.type,n.name))
727         for i in n.children.keys():
728                 checkconstraints(n.children[i],new_p,new_ra)
729
730 if service:
731         headerinput=pfilepath(header,allow_include=True)
732         userinput=sys.stdin.readlines()
733         pfile("user input",userinput)
734 else:
735         if inputfile is None:
736                 pfile("stdin",sys.stdin.readlines())
737         else:
738                 pfilepath(inputfile)
739
740 delempty(root)
741 checkconstraints(root,{},ipaddrset.complete_set())
742
743 if complaints>0:
744         if complaints==1: print("There was 1 problem.")
745         else: print("There were %d problems."%(complaints))
746         sys.exit(1)
747 complaints=None # arranges to crash if we complain later
748
749 if service:
750         # Put the user's input into their group file, and rebuild the main
751         # sites file
752         f=open(groupfiledir+"/T"+group.groupname(),'w')
753         f.write("# Section submitted by user %s, %s\n"%
754                 (user,time.asctime(time.localtime(time.time()))))
755         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
756         for i in userinput: f.write(i)
757         f.write("\n")
758         f.close()
759         os.rename(groupfiledir+"/T"+group.groupname(),
760                   groupfiledir+"/R"+group.groupname())
761         f=open(sitesfile+"-tmp",'w')
762         f.write("# sites file autogenerated by make-secnet-sites\n")
763         f.write("# generated %s, invoked by %s\n"%
764                 (time.asctime(time.localtime(time.time())),user))
765         f.write("# use make-secnet-sites to turn this file into a\n")
766         f.write("# valid /etc/secnet/sites.conf file\n\n")
767         for i in headerinput: f.write(i)
768         files=os.listdir(groupfiledir)
769         for i in files:
770                 if i[0]=='R':
771                         j=open(groupfiledir+"/"+i)
772                         f.write(j.read())
773                         j.close()
774         f.write("# end of sites file\n")
775         f.close()
776         os.rename(sitesfile+"-tmp",sitesfile)
777 else:
778         if outputfile is None:
779                 of=sys.stdout
780         else:
781                 tmp_outputfile=outputfile+'~tmp~'
782                 of=open(tmp_outputfile,'w')
783         outputsites(of)
784         if outputfile is not None:
785                 os.rename(tmp_outputfile,outputfile)