logging: Persistent children include pid in their log messages
[secnet] / make-secnet-sites
CommitLineData
3454dce4 1#! /usr/bin/env python
3b83c932 2# Copyright (C) 2001-2002 Stephen Early <steve@greenend.org.uk>
3454dce4
SE
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18"""VPN sites file manipulation.
19
20This program enables VPN site descriptions to be submitted for
21inclusion in a central database, and allows the resulting database to
22be turned into a secnet configuration file.
23
24A database file can be turned into a secnet configuration file simply:
25make-secnet-sites.py [infile [outfile]]
26
27It would be wise to run secnet with the "--just-check-config" option
28before installing the output on a live system.
29
30The program expects to be invoked via userv to manage the database; it
31relies on the USERV_USER and USERV_GROUP environment variables. The
32command line arguments for this invocation are:
33
34make-secnet-sites.py -u header-filename groupfiles-directory output-file \
35 group
36
37All but the last argument are expected to be set by userv; the 'group'
38argument is provided by the user. A suitable userv configuration file
39fragment is:
40
41reset
42no-disconnect-hup
43no-suppress-args
44cd ~/secnet/sites-test/
08f344d3 45execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
3454dce4
SE
46
47This program is part of secnet. It relies on the "ipaddr" library from
48Cendio Systems AB.
49
50"""
51
52import string
53import time
54import sys
55import os
3b83c932 56import getopt
040040f3 57import re
8dea8d37 58
71d65e4c
IJ
59import ipaddr
60
90ad8cd4
IJ
61sys.path.insert(0,"/usr/local/share/secnet")
62sys.path.insert(0,"/usr/share/secnet")
71d65e4c 63import ipaddrset
3454dce4 64
00152558 65VERSION="0.1.18"
3b83c932
SE
66
67# Classes describing possible datatypes in the configuration file
68
69class single_ipaddr:
70 "An IP address"
71 def __init__(self,w):
71d65e4c 72 self.addr=ipaddr.IPAddress(w[1])
3b83c932 73 def __str__(self):
71d65e4c 74 return '"%s"'%self.addr
3b83c932
SE
75
76class networks:
77 "A set of IP addresses specified as a list of networks"
3454dce4 78 def __init__(self,w):
71d65e4c 79 self.set=ipaddrset.IPAddressSet()
3454dce4 80 for i in w[1:]:
71d65e4c
IJ
81 x=ipaddr.IPNetwork(i,strict=True)
82 self.set.append([x])
3b83c932 83 def __str__(self):
71d65e4c 84 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
3454dce4
SE
85
86class dhgroup:
3b83c932 87 "A Diffie-Hellman group"
3454dce4 88 def __init__(self,w):
b2a56f7c
SE
89 self.mod=w[1]
90 self.gen=w[2]
3b83c932
SE
91 def __str__(self):
92 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
3454dce4
SE
93
94class hash:
3b83c932 95 "A choice of hash function"
3454dce4 96 def __init__(self,w):
b2a56f7c
SE
97 self.ht=w[1]
98 if (self.ht!='md5' and self.ht!='sha1'):
99 complain("unknown hash type %s"%(self.ht))
3b83c932
SE
100 def __str__(self):
101 return '%s'%(self.ht)
3454dce4
SE
102
103class email:
3b83c932 104 "An email address"
3454dce4 105 def __init__(self,w):
b2a56f7c 106 self.addr=w[1]
3b83c932
SE
107 def __str__(self):
108 return '<%s>'%(self.addr)
3454dce4 109
040040f3
IJ
110class boolean:
111 "A boolean"
112 def __init__(self,w):
113 if re.match('[TtYy1]',w[1]):
114 self.b=True
115 elif re.match('[FfNn0]',w[1]):
116 self.b=False
117 else:
118 complain("invalid boolean value");
119 def __str__(self):
120 return ['False','True'][self.b]
121
3454dce4 122class num:
3b83c932 123 "A decimal number"
3454dce4 124 def __init__(self,w):
b2a56f7c 125 self.n=string.atol(w[1])
3b83c932
SE
126 def __str__(self):
127 return '%d'%(self.n)
3454dce4
SE
128
129class address:
3b83c932 130 "A DNS name and UDP port number"
3454dce4 131 def __init__(self,w):
b2a56f7c
SE
132 self.adr=w[1]
133 self.port=string.atoi(w[2])
134 if (self.port<1 or self.port>65535):
135 complain("invalid port number")
3b83c932
SE
136 def __str__(self):
137 return '"%s"; port %d'%(self.adr,self.port)
3454dce4
SE
138
139class rsakey:
3b83c932 140 "An RSA public key"
3454dce4 141 def __init__(self,w):
b2a56f7c
SE
142 self.l=string.atoi(w[1])
143 self.e=w[2]
144 self.n=w[3]
3b83c932
SE
145 def __str__(self):
146 return 'rsa-public("%s","%s")'%(self.e,self.n)
147
148# Possible properties of configuration nodes
149keywords={
150 'contact':(email,"Contact address"),
151 'dh':(dhgroup,"Diffie-Hellman group"),
152 'hash':(hash,"Hash function"),
153 'key-lifetime':(num,"Maximum key lifetime (ms)"),
154 'setup-timeout':(num,"Key setup timeout (ms)"),
155 'setup-retries':(num,"Maximum key setup packet retries"),
156 'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
157 'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
158 'restrict-nets':(networks,"Allowable networks"),
159 'networks':(networks,"Claimed networks"),
160 'pubkey':(rsakey,"RSA public site key"),
161 'peer':(single_ipaddr,"Tunnel peer IP address"),
a25b1149 162 'address':(address,"External contact address and port"),
040040f3 163 'mobile':(boolean,"Site is mobile"),
3b83c932
SE
164}
165
166def sp(name,value):
167 "Simply output a property - the default case"
168 return "%s %s;\n"%(name,value)
169
170# All levels support these properties
171global_properties={
172 'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
173 'dh':sp,
174 'hash':sp,
175 'key-lifetime':sp,
176 'setup-timeout':sp,
177 'setup-retries':sp,
178 'wait-time':sp,
179 'renegotiate-time':sp,
a25b1149 180 'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
3b83c932
SE
181}
182
183class level:
184 "A level in the configuration hierarchy"
185 depth=0
186 leaf=0
187 allow_properties={}
188 require_properties={}
189 def __init__(self,w):
190 self.name=w[1]
191 self.properties={}
192 self.children={}
193 def indent(self,w,t):
194 w.write(" "[:t])
195 def prop_out(self,n):
196 return self.allow_properties[n](n,str(self.properties[n]))
197 def output_props(self,w,ind):
198 for i in self.properties.keys():
199 if self.allow_properties[i]:
200 self.indent(w,ind)
201 w.write("%s"%self.prop_out(i))
202 def output_data(self,w,ind,np):
203 self.indent(w,ind)
204 w.write("%s {\n"%(self.name))
205 self.output_props(w,ind+2)
206 if self.depth==1: w.write("\n");
207 for c in self.children.values():
208 c.output_data(w,ind+2,np+self.name+"/")
209 self.indent(w,ind)
210 w.write("};\n")
211
212class vpnlevel(level):
213 "VPN level in the configuration hierarchy"
214 depth=1
215 leaf=0
216 type="vpn"
217 allow_properties=global_properties.copy()
218 require_properties={
219 'contact':"VPN admin contact address"
220 }
221 def __init__(self,w):
222 level.__init__(self,w)
223 def output_vpnflat(self,w,ind,h):
224 "Output flattened list of site names for this VPN"
225 self.indent(w,ind)
226 w.write("%s {\n"%(self.name))
227 for i in self.children.keys():
228 self.children[i].output_vpnflat(w,ind+2,
229 h+"/"+self.name+"/"+i)
230 w.write("\n")
231 self.indent(w,ind+2)
232 w.write("all-sites %s;\n"%
233 string.join(self.children.keys(),','))
234 self.indent(w,ind)
235 w.write("};\n")
236
237class locationlevel(level):
238 "Location level in the configuration hierarchy"
239 depth=2
240 leaf=0
241 type="location"
242 allow_properties=global_properties.copy()
243 require_properties={
244 'contact':"Location admin contact address",
245 }
246 def __init__(self,w):
247 level.__init__(self,w)
248 self.group=w[2]
249 def output_vpnflat(self,w,ind,h):
250 self.indent(w,ind)
251 # The "h=h,self=self" abomination below exists because
252 # Python didn't support nested_scopes until version 2.1
253 w.write("%s %s;\n"%(self.name,string.join(
254 map(lambda x,h=h,self=self:
255 h+"/"+x,self.children.keys()),',')))
256
257class sitelevel(level):
258 "Site level (i.e. a leafnode) in the configuration hierarchy"
259 depth=3
260 leaf=1
261 type="site"
262 allow_properties=global_properties.copy()
263 allow_properties.update({
264 'address':sp,
265 'networks':None,
266 'peer':None,
a25b1149 267 'pubkey':(lambda n,v:"key %s;\n"%v),
2489e9eb 268 'address':(lambda n,v:"address %s;\n"%v),
040040f3 269 'mobile':sp,
3b83c932
SE
270 })
271 require_properties={
272 'dh':"Diffie-Hellman group",
273 'contact':"Site admin contact address",
3b83c932
SE
274 'networks':"Networks claimed by the site",
275 'hash':"hash function",
276 'peer':"Gateway address of the site",
a25b1149 277 'pubkey':"RSA public key of the site",
3b83c932 278 }
3454dce4 279 def __init__(self,w):
3b83c932
SE
280 level.__init__(self,w)
281 def output_data(self,w,ind,np):
282 self.indent(w,ind)
283 w.write("%s {\n"%(self.name))
284 self.indent(w,ind+2)
285 w.write("name \"%s\";\n"%(np+self.name))
286 self.output_props(w,ind+2)
287 self.indent(w,ind+2)
288 w.write("link netlink {\n");
289 self.indent(w,ind+4)
290 w.write("routes %s;\n"%str(self.properties["networks"]))
291 self.indent(w,ind+4)
292 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
293 self.indent(w,ind+2)
294 w.write("};\n")
295 self.indent(w,ind)
296 w.write("};\n")
297
298# Levels in the configuration file
299# (depth,properties)
300levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
301
302# Reserved vpn/location/site names
303reserved={'all-sites':None}
304reserved.update(keywords)
305reserved.update(levels)
3454dce4
SE
306
307def complain(msg):
3b83c932 308 "Complain about a particular input line"
3454dce4
SE
309 global complaints
310 print ("%s line %d: "%(file,line))+msg
311 complaints=complaints+1
312def moan(msg):
3b83c932 313 "Complain about something in general"
3454dce4
SE
314 global complaints
315 print msg;
316 complaints=complaints+1
317
3b83c932
SE
318root=level(['root','root']) # All vpns are children of this node
319obstack=[root]
320allow_defs=0 # Level above which new definitions are permitted
26f727b9 321prefix=''
3b83c932
SE
322
323def set_property(obj,w):
324 "Set a property on a configuration node"
325 if obj.properties.has_key(w[0]):
326 complain("%s %s already has property %s defined"%
327 (obj.type,obj.name,w[0]))
328 else:
329 obj.properties[w[0]]=keywords[w[0]][0](w)
3454dce4 330
c4497add 331def pline(i,allow_include=False):
3b83c932
SE
332 "Process a configuration file line"
333 global allow_defs, obstack, root
6d8cd9b2 334 w=string.split(i.rstrip('\n'))
433b0ae8 335 if len(w)==0: return [i]
3454dce4 336 keyword=w[0]
3b83c932 337 current=obstack[len(obstack)-1]
3454dce4 338 if keyword=='end-definitions':
3b83c932
SE
339 allow_defs=sitelevel.depth
340 obstack=[root]
433b0ae8 341 return [i]
c4497add
IJ
342 if keyword=='include':
343 if not allow_include:
344 complain("include not permitted here")
433b0ae8 345 return []
c4497add
IJ
346 if len(w) != 2:
347 complain("include requires one argument")
433b0ae8 348 return []
c4497add 349 newfile=os.path.join(os.path.dirname(file),w[1])
433b0ae8 350 return pfilepath(newfile,allow_include=allow_include)
3b83c932
SE
351 if levels.has_key(keyword):
352 # We may go up any number of levels, but only down by one
353 newdepth=levels[keyword].depth
354 currentdepth=len(obstack) # actually +1...
355 if newdepth<=currentdepth:
356 obstack=obstack[:newdepth]
357 if newdepth>currentdepth:
358 complain("May not go from level %d to level %d"%
359 (currentdepth-1,newdepth))
360 # See if it's a new one (and whether that's permitted)
361 # or an existing one
362 current=obstack[len(obstack)-1]
363 if current.children.has_key(w[1]):
364 # Not new
365 current=current.children[w[1]]
366 if service and group and current.depth==2:
367 if group!=current.group:
368 complain("Incorrect group!")
3454dce4 369 else:
3b83c932
SE
370 # New
371 # Ignore depth check for now
372 nl=levels[keyword](w)
373 if nl.depth<allow_defs:
374 complain("New definitions not allowed at "
375 "level %d"%nl.depth)
4a9b680b
IJ
376 # we risk crashing if we continue
377 sys.exit(1)
3b83c932
SE
378 current.children[w[1]]=nl
379 current=nl
380 obstack.append(current)
433b0ae8 381 return [i]
3b83c932
SE
382 if current.allow_properties.has_key(keyword):
383 set_property(current,w)
433b0ae8 384 return [i]
3454dce4 385 else:
3b83c932
SE
386 complain("Property %s not allowed at %s level"%
387 (keyword,current.type))
433b0ae8 388 return []
3b83c932
SE
389
390 complain("unknown keyword '%s'"%(keyword))
3454dce4 391
c4497add 392def pfilepath(pathname,allow_include=False):
9b8369e0 393 f=open(pathname)
433b0ae8 394 outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
9b8369e0 395 f.close()
433b0ae8 396 return outlines
9b8369e0 397
c4497add 398def pfile(name,lines,allow_include=False):
3b83c932 399 "Process a file"
3454dce4
SE
400 global file,line
401 file=name
402 line=0
433b0ae8 403 outlines=[]
3454dce4
SE
404 for i in lines:
405 line=line+1
406 if (i[0]=='#'): continue
433b0ae8
IJ
407 outlines += pline(i,allow_include=allow_include)
408 return outlines
3454dce4
SE
409
410def outputsites(w):
3b83c932
SE
411 "Output include file for secnet configuration"
412 w.write("# secnet sites file autogenerated by make-secnet-sites "
3454dce4 413 +"version %s\n"%VERSION)
3b83c932
SE
414 w.write("# %s\n"%time.asctime(time.localtime(time.time())))
415 w.write("# Command line: %s\n\n"%string.join(sys.argv))
3454dce4
SE
416
417 # Raw VPN data section of file
26f727b9 418 w.write(prefix+"vpn-data {\n")
3b83c932
SE
419 for i in root.children.values():
420 i.output_data(w,2,"")
3454dce4
SE
421 w.write("};\n")
422
423 # Per-VPN flattened lists
26f727b9 424 w.write(prefix+"vpn {\n")
3b83c932 425 for i in root.children.values():
26f727b9 426 i.output_vpnflat(w,2,prefix+"vpn-data")
3454dce4
SE
427 w.write("};\n")
428
429 # Flattened list of sites
26f727b9
IJ
430 w.write(prefix+"all-sites %s;\n"%string.join(
431 map(lambda x:"%svpn/%s/all-sites"%(prefix,x),
432 root.children.keys()),","))
3454dce4
SE
433
434# Are we being invoked from userv?
435service=0
436# If we are, which group does the caller want to modify?
437group=None
438
3454dce4
SE
439line=0
440file=None
441complaints=0
442
3454dce4
SE
443if len(sys.argv)<2:
444 pfile("stdin",sys.stdin.readlines())
445 of=sys.stdout
446else:
447 if sys.argv[1]=='-u':
448 if len(sys.argv)!=6:
449 print "Wrong number of arguments"
450 sys.exit(1)
451 service=1
452 header=sys.argv[2]
453 groupfiledir=sys.argv[3]
454 sitesfile=sys.argv[4]
455 group=sys.argv[5]
456 if not os.environ.has_key("USERV_USER"):
457 print "Environment variable USERV_USER not found"
458 sys.exit(1)
459 user=os.environ["USERV_USER"]
460 # Check that group is in USERV_GROUP
461 if not os.environ.has_key("USERV_GROUP"):
462 print "Environment variable USERV_GROUP not found"
463 sys.exit(1)
464 ugs=os.environ["USERV_GROUP"]
465 ok=0
466 for i in string.split(ugs):
467 if group==i: ok=1
468 if not ok:
469 print "caller not in group %s"%group
470 sys.exit(1)
5b77d1a9 471 headerinput=pfilepath(header,allow_include=True)
3454dce4
SE
472 userinput=sys.stdin.readlines()
473 pfile("user input",userinput)
474 else:
26f727b9
IJ
475 if sys.argv[1]=='-P':
476 prefix=sys.argv[2]
477 sys.argv[1:3]=[]
3454dce4
SE
478 if len(sys.argv)>3:
479 print "Too many arguments"
480 sys.exit(1)
21fd3a92 481 pfilepath(sys.argv[1])
3454dce4
SE
482 of=sys.stdout
483 if len(sys.argv)>2:
484 of=open(sys.argv[2],'w')
485
486# Sanity check section
3b83c932
SE
487# Delete nodes where leaf=0 that have no children
488
489def live(n):
490 "Number of leafnodes below node n"
491 if n.leaf: return 1
492 for i in n.children.keys():
493 if live(n.children[i]): return 1
494 return 0
495def delempty(n):
496 "Delete nodes that have no leafnode children"
497 for i in n.children.keys():
498 delempty(n.children[i])
499 if not live(n.children[i]):
500 del n.children[i]
501delempty(root)
502
503# Check that all constraints are met (as far as I can tell
504# restrict-nets/networks/peer are the only special cases)
505
506def checkconstraints(n,p,ra):
507 new_p=p.copy()
508 new_p.update(n.properties)
509 for i in n.require_properties.keys():
510 if not new_p.has_key(i):
511 moan("%s %s is missing property %s"%
512 (n.type,n.name,i))
513 for i in new_p.keys():
514 if not n.allow_properties.has_key(i):
515 moan("%s %s has forbidden property %s"%
516 (n.type,n.name,i))
517 # Check address range restrictions
518 if n.properties.has_key("restrict-nets"):
519 new_ra=ra.intersection(n.properties["restrict-nets"].set)
3454dce4 520 else:
3b83c932
SE
521 new_ra=ra
522 if n.properties.has_key("networks"):
71d65e4c 523 if not n.properties["networks"].set <= new_ra:
3b83c932
SE
524 moan("%s %s networks out of bounds"%(n.type,n.name))
525 if n.properties.has_key("peer"):
526 if not n.properties["networks"].set.contains(
527 n.properties["peer"].addr):
528 moan("%s %s peer not in networks"%(n.type,n.name))
529 for i in n.children.keys():
530 checkconstraints(n.children[i],new_p,new_ra)
531
71d65e4c 532checkconstraints(root,{},ipaddrset.complete_set())
3454dce4
SE
533
534if complaints>0:
535 if complaints==1: print "There was 1 problem."
536 else: print "There were %d problems."%(complaints)
537 sys.exit(1)
538
539if service:
540 # Put the user's input into their group file, and rebuild the main
541 # sites file
08f344d3 542 f=open(groupfiledir+"/T"+group,'w')
3454dce4
SE
543 f.write("# Section submitted by user %s, %s\n"%
544 (user,time.asctime(time.localtime(time.time()))))
3b83c932 545 f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
3454dce4
SE
546 for i in userinput: f.write(i)
547 f.write("\n")
548 f.close()
08f344d3
SE
549 os.rename(groupfiledir+"/T"+group,groupfiledir+"/R"+group)
550 f=open(sitesfile+"-tmp",'w')
ff05a229 551 f.write("# sites file autogenerated by make-secnet-sites\n")
08f344d3
SE
552 f.write("# generated %s, invoked by %s\n"%
553 (time.asctime(time.localtime(time.time())),user))
ff05a229 554 f.write("# use make-secnet-sites to turn this file into a\n")
08f344d3
SE
555 f.write("# valid /etc/secnet/sites.conf file\n\n")
556 for i in headerinput: f.write(i)
557 files=os.listdir(groupfiledir)
558 for i in files:
559 if i[0]=='R':
560 j=open(groupfiledir+"/"+i)
561 f.write(j.read())
562 j.close()
563 f.write("# end of sites file\n")
564 f.close()
565 os.rename(sitesfile+"-tmp",sitesfile)
3454dce4
SE
566else:
567 outputsites(of)