chiark / gitweb /
make-secnet-sites: output_vpnflat: Change recursive variables
[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(outputfile,'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,path):
439                 "Output flattened list of site names for this VPN"
440                 ind=2*(len(path)+1)
441                 self.indent(w,ind)
442                 w.write("%s {\n"%(self.name))
443                 for i in self.children.keys():
444                         self.children[i].output_vpnflat(w,path+(self,))
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,path):
465                 ind=2*(len(path)+1)
466                 self.indent(w,ind)
467                 # The "path=path,self=self" abomination below exists because
468                 # Python didn't support nested_scopes until version 2.1
469                 #
470                 #"/"+self.name+"/"+i
471                 w.write("%s %s;\n"%(self.name,','.join(
472                         map(lambda x,path=path,self=self:
473                             '/'.join([prefix+"vpn-data"] + list(map(
474                                     lambda i: i.name,
475                                     path+(self,x)))),
476                             self.children.values()))))
477
478 class sitelevel(level):
479         "Site level (i.e. a leafnode) in the configuration hierarchy"
480         depth=3
481         leaf=1
482         type="site"
483         allow_properties=global_properties.copy()
484         allow_properties.update({
485          'address':sp,
486          'networks':None,
487          'peer':None,
488          'pubkey':(lambda n,v:"key %s;\n"%v),
489          'mobile':sp,
490         })
491         require_properties={
492          'dh':"Diffie-Hellman group",
493          'contact':"Site admin contact address",
494          'networks':"Networks claimed by the site",
495          'hash':"hash function",
496          'peer':"Gateway address of the site",
497          'pubkey':"RSA public key of the site",
498         }
499         def __init__(self,w):
500                 level.__init__(self,w)
501         def output_data(self,w,ind,np):
502                 self.indent(w,ind)
503                 w.write("%s {\n"%(self.name))
504                 self.indent(w,ind+2)
505                 w.write("name \"%s\";\n"%(np+self.name))
506                 self.output_props(w,ind+2)
507                 self.indent(w,ind+2)
508                 w.write("link netlink {\n");
509                 self.indent(w,ind+4)
510                 w.write("routes %s;\n"%str(self.properties["networks"]))
511                 self.indent(w,ind+4)
512                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
513                 self.indent(w,ind+2)
514                 w.write("};\n")
515                 self.indent(w,ind)
516                 w.write("};\n")
517
518 # Levels in the configuration file
519 # (depth,properties)
520 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
521
522 # Reserved vpn/location/site names
523 reserved={'all-sites':None}
524 reserved.update(keywords)
525 reserved.update(levels)
526
527 def complain(msg):
528         "Complain about a particular input line"
529         global complaints
530         print(("%s line %d: "%(file,line))+msg)
531         complaints=complaints+1
532 def moan(msg):
533         "Complain about something in general"
534         global complaints
535         print(msg);
536         complaints=complaints+1
537
538 class UntaintedRoot():
539         def __init__(self,s): self._s=s
540         def name(self): return self._s
541         def keyword(self): return self._s
542
543 root=level([UntaintedRoot(x) for x in ['root','root']])
544 # All vpns are children of this node
545 obstack=[root]
546 allow_defs=0   # Level above which new definitions are permitted
547 prefix=''
548
549 def set_property(obj,w):
550         "Set a property on a configuration node"
551         prop=w[0]
552         if prop.raw() in obj.properties:
553                 obj.properties[prop.raw_mark_ok()].add(obj,w)
554         else:
555                 obj.properties[prop.raw()]=keywords[prop.raw_mark_ok()][0](w)
556
557
558 def pline(il,allow_include=False):
559         "Process a configuration file line"
560         global allow_defs, obstack, root
561         w=il.rstrip('\n').split()
562         if len(w)==0: return ['']
563         w=list([Tainted(x) for x in w])
564         keyword=w[0]
565         current=obstack[len(obstack)-1]
566         copyout=lambda: ['    '*len(obstack) +
567                         ' '.join([ww.output() for ww in w]) +
568                         '\n']
569         if keyword=='end-definitions':
570                 keyword.raw_mark_ok()
571                 allow_defs=sitelevel.depth
572                 obstack=[root]
573                 return copyout()
574         if keyword=='include':
575                 if not allow_include:
576                         complain("include not permitted here")
577                         return []
578                 if len(w) != 2:
579                         complain("include requires one argument")
580                         return []
581                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
582                 # ^ user of "include" is trusted so raw_mark_ok is good
583                 return pfilepath(newfile,allow_include=allow_include)
584         if keyword.raw() in levels:
585                 # We may go up any number of levels, but only down by one
586                 newdepth=levels[keyword.raw_mark_ok()].depth
587                 currentdepth=len(obstack) # actually +1...
588                 if newdepth<=currentdepth:
589                         obstack=obstack[:newdepth]
590                 if newdepth>currentdepth:
591                         complain("May not go from level %d to level %d"%
592                                 (currentdepth-1,newdepth))
593                 # See if it's a new one (and whether that's permitted)
594                 # or an existing one
595                 current=obstack[len(obstack)-1]
596                 tname=w[1].name()
597                 if tname in current.children:
598                         # Not new
599                         current=current.children[tname]
600                         if service and group and current.depth==2:
601                                 if group!=current.group:
602                                         complain("Incorrect group!")
603                                 w[2].groupname()
604                 else:
605                         # New
606                         # Ignore depth check for now
607                         nl=levels[keyword.raw()](w)
608                         if nl.depth<allow_defs:
609                                 complain("New definitions not allowed at "
610                                         "level %d"%nl.depth)
611                                 # we risk crashing if we continue
612                                 sys.exit(1)
613                         current.children[tname]=nl
614                         current=nl
615                 obstack.append(current)
616                 return copyout()
617         if keyword.raw() not in current.allow_properties:
618                 complain("Property %s not allowed at %s level"%
619                         (keyword.raw(),current.type))
620                 return []
621         elif current.depth == vpnlevel.depth < allow_defs:
622                 complain("Not allowed to set VPN properties here")
623                 return []
624         else:
625                 set_property(current,w)
626                 return copyout()
627
628         complain("unknown keyword '%s'"%(keyword.raw()))
629
630 def pfilepath(pathname,allow_include=False):
631         f=open(pathname)
632         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
633         f.close()
634         return outlines
635
636 def pfile(name,lines,allow_include=False):
637         "Process a file"
638         global file,line
639         file=name
640         line=0
641         outlines=[]
642         for i in lines:
643                 line=line+1
644                 if (i[0]=='#'): continue
645                 outlines += pline(i,allow_include=allow_include)
646         return outlines
647
648 def outputsites(w):
649         "Output include file for secnet configuration"
650         w.write("# secnet sites file autogenerated by make-secnet-sites "
651                 +"version %s\n"%VERSION)
652         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
653         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
654
655         # Raw VPN data section of file
656         w.write(prefix+"vpn-data {\n")
657         for i in root.children.values():
658                 i.output_data(w,2,"")
659         w.write("};\n")
660
661         # Per-VPN flattened lists
662         w.write(prefix+"vpn {\n")
663         for i in root.children.values():
664                 i.output_vpnflat(w,())
665         w.write("};\n")
666
667         # Flattened list of sites
668         w.write(prefix+"all-sites %s;\n"%",".join(
669                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x),
670                         root.children.keys())))
671
672 line=0
673 file=None
674 complaints=0
675
676 # Sanity check section
677 # Delete nodes where leaf=0 that have no children
678
679 def live(n):
680         "Number of leafnodes below node n"
681         if n.leaf: return 1
682         for i in n.children.keys():
683                 if live(n.children[i]): return 1
684         return 0
685 def delempty(n):
686         "Delete nodes that have no leafnode children"
687         for i in list(n.children.keys()):
688                 delempty(n.children[i])
689                 if not live(n.children[i]):
690                         del n.children[i]
691
692 # Check that all constraints are met (as far as I can tell
693 # restrict-nets/networks/peer are the only special cases)
694
695 def checkconstraints(n,p,ra):
696         new_p=p.copy()
697         new_p.update(n.properties)
698         for i in n.require_properties.keys():
699                 if i not in new_p:
700                         moan("%s %s is missing property %s"%
701                                 (n.type,n.name,i))
702         for i in new_p.keys():
703                 if i not in n.allow_properties:
704                         moan("%s %s has forbidden property %s"%
705                                 (n.type,n.name,i))
706         # Check address range restrictions
707         if "restrict-nets" in n.properties:
708                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
709         else:
710                 new_ra=ra
711         if "networks" in n.properties:
712                 if not n.properties["networks"].set <= new_ra:
713                         moan("%s %s networks out of bounds"%(n.type,n.name))
714                 if "peer" in n.properties:
715                         if not n.properties["networks"].set.contains(
716                                 n.properties["peer"].addr):
717                                 moan("%s %s peer not in networks"%(n.type,n.name))
718         for i in n.children.keys():
719                 checkconstraints(n.children[i],new_p,new_ra)
720
721 if service:
722         headerinput=pfilepath(header,allow_include=True)
723         userinput=sys.stdin.readlines()
724         pfile("user input",userinput)
725 else:
726         if inputfile is None:
727                 pfile("stdin",sys.stdin.readlines())
728         else:
729                 pfilepath(inputfile)
730
731 delempty(root)
732 checkconstraints(root,{},ipaddrset.complete_set())
733
734 if complaints>0:
735         if complaints==1: print("There was 1 problem.")
736         else: print("There were %d problems."%(complaints))
737         sys.exit(1)
738 complaints=None # arranges to crash if we complain later
739
740 if service:
741         # Put the user's input into their group file, and rebuild the main
742         # sites file
743         f=open(groupfiledir+"/T"+group.groupname(),'w')
744         f.write("# Section submitted by user %s, %s\n"%
745                 (user,time.asctime(time.localtime(time.time()))))
746         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
747         for i in userinput: f.write(i)
748         f.write("\n")
749         f.close()
750         os.rename(groupfiledir+"/T"+group.groupname(),
751                   groupfiledir+"/R"+group.groupname())
752         f=open(sitesfile+"-tmp",'w')
753         f.write("# sites file autogenerated by make-secnet-sites\n")
754         f.write("# generated %s, invoked by %s\n"%
755                 (time.asctime(time.localtime(time.time())),user))
756         f.write("# use make-secnet-sites to turn this file into a\n")
757         f.write("# valid /etc/secnet/sites.conf file\n\n")
758         for i in headerinput: f.write(i)
759         files=os.listdir(groupfiledir)
760         for i in files:
761                 if i[0]=='R':
762                         j=open(groupfiledir+"/"+i)
763                         f.write(j.read())
764                         j.close()
765         f.write("# end of sites file\n")
766         f.close()
767         os.rename(sitesfile+"-tmp",sitesfile)
768 else:
769         outputsites(of)