chiark / gitweb /
make-secnet-sites: allow Tainted construction to specify file/line
[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                 if "USERV_USER" not in os.environ:
231                         print("Environment variable USERV_USER not found")
232                         sys.exit(1)
233                 user=os.environ["USERV_USER"]
234                 # Check that group is in USERV_GROUP
235                 if "USERV_GROUP" not in os.environ:
236                         print("Environment variable USERV_GROUP not found")
237                         sys.exit(1)
238                 ugs=os.environ["USERV_GROUP"]
239                 ok=0
240                 for i in ugs.split():
241                         if group==i: ok=1
242                 if not ok:
243                         print("caller not in group %s"%group)
244                         sys.exit(1)
245         else:
246                 if len(av.arg)>3:
247                         print("Too many arguments")
248                         sys.exit(1)
249                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
250                 if outputfile is None: of=sys.stdout
251                 else: of=open(sys.argv[2],'w')
252
253 parse_args()
254
255 # Classes describing possible datatypes in the configuration file
256
257 class basetype:
258         "Common protocol for configuration types."
259         def add(self,obj,w):
260                 complain("%s %s already has property %s defined"%
261                         (obj.type,obj.name,w[0].raw()))
262
263 class conflist:
264         "A list of some kind of configuration type."
265         def __init__(self,subtype,w):
266                 self.subtype=subtype
267                 self.list=[subtype(w)]
268         def add(self,obj,w):
269                 self.list.append(self.subtype(w))
270         def __str__(self):
271                 return ', '.join(map(str, self.list))
272 def listof(subtype):
273         return lambda w: conflist(subtype, w)
274
275 class single_ipaddr (basetype):
276         "An IP address"
277         def __init__(self,w):
278                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
279         def __str__(self):
280                 return '"%s"'%self.addr
281
282 class networks (basetype):
283         "A set of IP addresses specified as a list of networks"
284         def __init__(self,w):
285                 self.set=ipaddrset.IPAddressSet()
286                 for i in w[1:]:
287                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
288                         self.set.append([x])
289         def __str__(self):
290                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
291
292 class dhgroup (basetype):
293         "A Diffie-Hellman group"
294         def __init__(self,w):
295                 self.mod=w[1].bignum_16('dh','dh mod')
296                 self.gen=w[2].bignum_16('dh','dh gen')
297         def __str__(self):
298                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
299
300 class hash (basetype):
301         "A choice of hash function"
302         def __init__(self,w):
303                 hname=w[1]
304                 self.ht=hname.raw()
305                 if (self.ht!='md5' and self.ht!='sha1'):
306                         complain("unknown hash type %s"%(self.ht))
307                         self.ht=None
308                 else:
309                         hname.raw_mark_ok()
310         def __str__(self):
311                 return '%s'%(self.ht)
312
313 class email (basetype):
314         "An email address"
315         def __init__(self,w):
316                 self.addr=w[1].email()
317         def __str__(self):
318                 return '<%s>'%(self.addr)
319
320 class boolean (basetype):
321         "A boolean"
322         def __init__(self,w):
323                 v=w[1]
324                 if re.match('[TtYy1]',v.raw()):
325                         self.b=True
326                         v.raw_mark_ok()
327                 elif re.match('[FfNn0]',v.raw()):
328                         self.b=False
329                         v.raw_mark_ok()
330                 else:
331                         complain("invalid boolean value");
332         def __str__(self):
333                 return ['False','True'][self.b]
334
335 class num (basetype):
336         "A decimal number"
337         def __init__(self,w):
338                 self.n=w[1].number(0,0x7fffffff)
339         def __str__(self):
340                 return '%d'%(self.n)
341
342 class address (basetype):
343         "A DNS name and UDP port number"
344         def __init__(self,w):
345                 self.adr=w[1].host()
346                 self.port=w[2].number(1,65536,'port')
347         def __str__(self):
348                 return '"%s"; port %d'%(self.adr,self.port)
349
350 class rsakey (basetype):
351         "An RSA public key"
352         def __init__(self,w):
353                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
354                 self.e=w[2].bignum_10('rsa','rsa e')
355                 self.n=w[3].bignum_10('rsa','rsa n')
356                 if len(w) >= 5: w[4].email()
357         def __str__(self):
358                 return 'rsa-public("%s","%s")'%(self.e,self.n)
359
360 # Possible properties of configuration nodes
361 keywords={
362  'contact':(email,"Contact address"),
363  'dh':(dhgroup,"Diffie-Hellman group"),
364  'hash':(hash,"Hash function"),
365  'key-lifetime':(num,"Maximum key lifetime (ms)"),
366  'setup-timeout':(num,"Key setup timeout (ms)"),
367  'setup-retries':(num,"Maximum key setup packet retries"),
368  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
369  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
370  'restrict-nets':(networks,"Allowable networks"),
371  'networks':(networks,"Claimed networks"),
372  'pubkey':(rsakey,"RSA public site key"),
373  'peer':(single_ipaddr,"Tunnel peer IP address"),
374  'address':(address,"External contact address and port"),
375  'mobile':(boolean,"Site is mobile"),
376 }
377
378 def sp(name,value):
379         "Simply output a property - the default case"
380         return "%s %s;\n"%(name,value)
381
382 # All levels support these properties
383 global_properties={
384         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
385         'dh':sp,
386         'hash':sp,
387         'key-lifetime':sp,
388         'setup-timeout':sp,
389         'setup-retries':sp,
390         'wait-time':sp,
391         'renegotiate-time':sp,
392         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
393 }
394
395 class level:
396         "A level in the configuration hierarchy"
397         depth=0
398         leaf=0
399         allow_properties={}
400         require_properties={}
401         def __init__(self,w):
402                 self.type=w[0].keyword()
403                 self.name=w[1].name()
404                 self.properties={}
405                 self.children={}
406         def indent(self,w,t):
407                 w.write("                 "[:t])
408         def prop_out(self,n):
409                 return self.allow_properties[n](n,str(self.properties[n]))
410         def output_props(self,w,ind):
411                 for i in self.properties.keys():
412                         if self.allow_properties[i]:
413                                 self.indent(w,ind)
414                                 w.write("%s"%self.prop_out(i))
415         def output_data(self,w,ind,np):
416                 self.indent(w,ind)
417                 w.write("%s {\n"%(self.name))
418                 self.output_props(w,ind+2)
419                 if self.depth==1: w.write("\n");
420                 for c in self.children.values():
421                         c.output_data(w,ind+2,np+self.name+"/")
422                 self.indent(w,ind)
423                 w.write("};\n")
424
425 class vpnlevel(level):
426         "VPN level in the configuration hierarchy"
427         depth=1
428         leaf=0
429         type="vpn"
430         allow_properties=global_properties.copy()
431         require_properties={
432          'contact':"VPN admin contact address"
433         }
434         def __init__(self,w):
435                 level.__init__(self,w)
436         def output_vpnflat(self,w,ind,h):
437                 "Output flattened list of site names for this VPN"
438                 self.indent(w,ind)
439                 w.write("%s {\n"%(self.name))
440                 for i in self.children.keys():
441                         self.children[i].output_vpnflat(w,ind+2,
442                                 h+"/"+self.name+"/"+i)
443                 w.write("\n")
444                 self.indent(w,ind+2)
445                 w.write("all-sites %s;\n"%
446                         ','.join(self.children.keys()))
447                 self.indent(w,ind)
448                 w.write("};\n")
449
450 class locationlevel(level):
451         "Location level in the configuration hierarchy"
452         depth=2
453         leaf=0
454         type="location"
455         allow_properties=global_properties.copy()
456         require_properties={
457          'contact':"Location admin contact address",
458         }
459         def __init__(self,w):
460                 level.__init__(self,w)
461                 self.group=w[2].groupname()
462         def output_vpnflat(self,w,ind,h):
463                 self.indent(w,ind)
464                 # The "h=h,self=self" abomination below exists because
465                 # Python didn't support nested_scopes until version 2.1
466                 w.write("%s %s;\n"%(self.name,','.join(
467                         map(lambda x,h=h,self=self:
468                                 h+"/"+x,self.children.keys()))))
469
470 class sitelevel(level):
471         "Site level (i.e. a leafnode) in the configuration hierarchy"
472         depth=3
473         leaf=1
474         type="site"
475         allow_properties=global_properties.copy()
476         allow_properties.update({
477          'address':sp,
478          'networks':None,
479          'peer':None,
480          'pubkey':(lambda n,v:"key %s;\n"%v),
481          'mobile':sp,
482         })
483         require_properties={
484          'dh':"Diffie-Hellman group",
485          'contact':"Site admin contact address",
486          'networks':"Networks claimed by the site",
487          'hash':"hash function",
488          'peer':"Gateway address of the site",
489          'pubkey':"RSA public key of the site",
490         }
491         def __init__(self,w):
492                 level.__init__(self,w)
493         def output_data(self,w,ind,np):
494                 self.indent(w,ind)
495                 w.write("%s {\n"%(self.name))
496                 self.indent(w,ind+2)
497                 w.write("name \"%s\";\n"%(np+self.name))
498                 self.output_props(w,ind+2)
499                 self.indent(w,ind+2)
500                 w.write("link netlink {\n");
501                 self.indent(w,ind+4)
502                 w.write("routes %s;\n"%str(self.properties["networks"]))
503                 self.indent(w,ind+4)
504                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
505                 self.indent(w,ind+2)
506                 w.write("};\n")
507                 self.indent(w,ind)
508                 w.write("};\n")
509
510 # Levels in the configuration file
511 # (depth,properties)
512 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
513
514 # Reserved vpn/location/site names
515 reserved={'all-sites':None}
516 reserved.update(keywords)
517 reserved.update(levels)
518
519 def complain(msg):
520         "Complain about a particular input line"
521         global complaints
522         print(("%s line %d: "%(file,line))+msg)
523         complaints=complaints+1
524 def moan(msg):
525         "Complain about something in general"
526         global complaints
527         print(msg);
528         complaints=complaints+1
529
530 class UntaintedRoot():
531         def __init__(self,s): self._s=s
532         def name(self): return self._s
533         def keyword(self): return self._s
534
535 root=level([UntaintedRoot(x) for x in ['root','root']])
536 # All vpns are children of this node
537 obstack=[root]
538 allow_defs=0   # Level above which new definitions are permitted
539 prefix=''
540
541 def set_property(obj,w):
542         "Set a property on a configuration node"
543         prop=w[0]
544         if prop.raw() in obj.properties:
545                 obj.properties[prop.raw_mark_ok()].add(obj,w)
546         else:
547                 obj.properties[prop.raw()]=keywords[prop.raw_mark_ok()][0](w)
548
549
550 def pline(il,allow_include=False):
551         "Process a configuration file line"
552         global allow_defs, obstack, root
553         w=il.rstrip('\n').split()
554         if len(w)==0: return ['']
555         w=list([Tainted(x) for x in w])
556         keyword=w[0]
557         current=obstack[len(obstack)-1]
558         copyout=lambda: ['    '*len(obstack) +
559                         ' '.join([ww.output() for ww in w]) +
560                         '\n']
561         if keyword=='end-definitions':
562                 keyword.raw_mark_ok()
563                 allow_defs=sitelevel.depth
564                 obstack=[root]
565                 return copyout()
566         if keyword=='include':
567                 if not allow_include:
568                         complain("include not permitted here")
569                         return []
570                 if len(w) != 2:
571                         complain("include requires one argument")
572                         return []
573                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
574                 # ^ user of "include" is trusted so raw_mark_ok is good
575                 return pfilepath(newfile,allow_include=allow_include)
576         if keyword.raw() in levels:
577                 # We may go up any number of levels, but only down by one
578                 newdepth=levels[keyword.raw_mark_ok()].depth
579                 currentdepth=len(obstack) # actually +1...
580                 if newdepth<=currentdepth:
581                         obstack=obstack[:newdepth]
582                 if newdepth>currentdepth:
583                         complain("May not go from level %d to level %d"%
584                                 (currentdepth-1,newdepth))
585                 # See if it's a new one (and whether that's permitted)
586                 # or an existing one
587                 current=obstack[len(obstack)-1]
588                 tname=w[1].name()
589                 if tname in current.children:
590                         # Not new
591                         current=current.children[tname]
592                         if service and group and current.depth==2:
593                                 if group!=current.group:
594                                         complain("Incorrect group!")
595                                 w[2].groupname()
596                 else:
597                         # New
598                         # Ignore depth check for now
599                         nl=levels[keyword.raw()](w)
600                         if nl.depth<allow_defs:
601                                 complain("New definitions not allowed at "
602                                         "level %d"%nl.depth)
603                                 # we risk crashing if we continue
604                                 sys.exit(1)
605                         current.children[tname]=nl
606                         current=nl
607                 obstack.append(current)
608                 return copyout()
609         if keyword.raw() not in current.allow_properties:
610                 complain("Property %s not allowed at %s level"%
611                         (keyword.raw(),current.type))
612                 return []
613         elif current.depth == vpnlevel.depth < allow_defs:
614                 complain("Not allowed to set VPN properties here")
615                 return []
616         else:
617                 set_property(current,w)
618                 return copyout()
619
620         complain("unknown keyword '%s'"%(keyword.raw()))
621
622 def pfilepath(pathname,allow_include=False):
623         f=open(pathname)
624         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
625         f.close()
626         return outlines
627
628 def pfile(name,lines,allow_include=False):
629         "Process a file"
630         global file,line
631         file=name
632         line=0
633         outlines=[]
634         for i in lines:
635                 line=line+1
636                 if (i[0]=='#'): continue
637                 outlines += pline(i,allow_include=allow_include)
638         return outlines
639
640 def outputsites(w):
641         "Output include file for secnet configuration"
642         w.write("# secnet sites file autogenerated by make-secnet-sites "
643                 +"version %s\n"%VERSION)
644         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
645         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
646
647         # Raw VPN data section of file
648         w.write(prefix+"vpn-data {\n")
649         for i in root.children.values():
650                 i.output_data(w,2,"")
651         w.write("};\n")
652
653         # Per-VPN flattened lists
654         w.write(prefix+"vpn {\n")
655         for i in root.children.values():
656                 i.output_vpnflat(w,2,prefix+"vpn-data")
657         w.write("};\n")
658
659         # Flattened list of sites
660         w.write(prefix+"all-sites %s;\n"%",".join(
661                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x),
662                         root.children.keys())))
663
664 line=0
665 file=None
666 complaints=0
667
668 # Sanity check section
669 # Delete nodes where leaf=0 that have no children
670
671 def live(n):
672         "Number of leafnodes below node n"
673         if n.leaf: return 1
674         for i in n.children.keys():
675                 if live(n.children[i]): return 1
676         return 0
677 def delempty(n):
678         "Delete nodes that have no leafnode children"
679         for i in list(n.children.keys()):
680                 delempty(n.children[i])
681                 if not live(n.children[i]):
682                         del n.children[i]
683
684 # Check that all constraints are met (as far as I can tell
685 # restrict-nets/networks/peer are the only special cases)
686
687 def checkconstraints(n,p,ra):
688         new_p=p.copy()
689         new_p.update(n.properties)
690         for i in n.require_properties.keys():
691                 if i not in new_p:
692                         moan("%s %s is missing property %s"%
693                                 (n.type,n.name,i))
694         for i in new_p.keys():
695                 if i not in n.allow_properties:
696                         moan("%s %s has forbidden property %s"%
697                                 (n.type,n.name,i))
698         # Check address range restrictions
699         if "restrict-nets" in n.properties:
700                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
701         else:
702                 new_ra=ra
703         if "networks" in n.properties:
704                 if not n.properties["networks"].set <= new_ra:
705                         moan("%s %s networks out of bounds"%(n.type,n.name))
706                 if "peer" in n.properties:
707                         if not n.properties["networks"].set.contains(
708                                 n.properties["peer"].addr):
709                                 moan("%s %s peer not in networks"%(n.type,n.name))
710         for i in n.children.keys():
711                 checkconstraints(n.children[i],new_p,new_ra)
712
713 if service:
714         headerinput=pfilepath(header,allow_include=True)
715         userinput=sys.stdin.readlines()
716         pfile("user input",userinput)
717 else:
718         if inputfile is None:
719                 pfile("stdin",sys.stdin.readlines())
720         else:
721                 pfilepath(inputfile)
722
723 delempty(root)
724 checkconstraints(root,{},ipaddrset.complete_set())
725
726 if complaints>0:
727         if complaints==1: print("There was 1 problem.")
728         else: print("There were %d problems."%(complaints))
729         sys.exit(1)
730 complaints=None # arranges to crash if we complain later
731
732 if service:
733         # Put the user's input into their group file, and rebuild the main
734         # sites file
735         f=open(groupfiledir+"/T"+group,'w')
736         f.write("# Section submitted by user %s, %s\n"%
737                 (user,time.asctime(time.localtime(time.time()))))
738         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
739         for i in userinput: f.write(i)
740         f.write("\n")
741         f.close()
742         os.rename(groupfiledir+"/T"+group,groupfiledir+"/R"+group)
743         f=open(sitesfile+"-tmp",'w')
744         f.write("# sites file autogenerated by make-secnet-sites\n")
745         f.write("# generated %s, invoked by %s\n"%
746                 (time.asctime(time.localtime(time.time())),user))
747         f.write("# use make-secnet-sites to turn this file into a\n")
748         f.write("# valid /etc/secnet/sites.conf file\n\n")
749         for i in headerinput: f.write(i)
750         files=os.listdir(groupfiledir)
751         for i in files:
752                 if i[0]=='R':
753                         j=open(groupfiledir+"/"+i)
754                         f.write(j.read())
755                         j.close()
756         f.write("# end of sites file\n")
757         f.close()
758         os.rename(sitesfile+"-tmp",sitesfile)
759 else:
760         outputsites(of)