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