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