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