Import release 0.1.8
[secnet] / make-secnet-sites.py
CommitLineData
3454dce4
SE
1#! /usr/bin/env python
2# Copyright (C) 2001 Stephen Early <steve@greenend.org.uk>
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
56import ipaddr
57
b2a56f7c 58VERSION="0.1.5"
3454dce4
SE
59
60class vpn:
61 def __init__(self,name):
62 self.name=name
63 self.allow_defs=0
64 self.locations={}
65 self.defs={}
66
67class location:
68 def __init__(self,name,vpn):
69 self.group=None
70 self.name=name
71 self.allow_defs=1
72 self.vpn=vpn
73 self.sites={}
74 self.defs={}
75
76class site:
77 def __init__(self,name,location):
78 self.name=name
79 self.allow_defs=1
80 self.location=location
81 self.defs={}
82
83class nets:
84 def __init__(self,w):
85 self.w=w
86 self.set=ipaddr.ip_set()
87 for i in w[1:]:
88 x=string.split(i,"/")
89 self.set.append(ipaddr.network(x[0],x[1],
90 ipaddr.DEMAND_NETWORK))
91 def subsetof(self,s):
92 # I'd like to do this:
93 # return self.set.is_subset(s)
94 # but there isn't an is_subset() method
95 # Instead we see if we intersect with the complement of s
96 sc=s.set.complement()
97 i=sc.intersection(self.set)
98 return i.is_empty()
99 def out(self):
100 rn=''
101 if (self.w[0]=='restrict-nets'): rn='# '
102 return '%s%s %s;'%(rn,self.w[0],
103 string.join(map(lambda x:'"%s/%s"'%(x.ip_str(),
104 x.mask.netmask_bits_str),
105 self.set.as_list_of_networks()),","))
106
107class dhgroup:
108 def __init__(self,w):
b2a56f7c
SE
109 self.mod=w[1]
110 self.gen=w[2]
3454dce4 111 def out(self):
b2a56f7c 112 return 'dh diffie-hellman("%s","%s");'%(self.mod,self.gen)
3454dce4
SE
113
114class hash:
115 def __init__(self,w):
b2a56f7c
SE
116 self.ht=w[1]
117 if (self.ht!='md5' and self.ht!='sha1'):
118 complain("unknown hash type %s"%(self.ht))
3454dce4 119 def out(self):
b2a56f7c 120 return 'hash %s;'%(self.ht)
3454dce4
SE
121
122class email:
123 def __init__(self,w):
b2a56f7c 124 self.addr=w[1]
3454dce4 125 def out(self):
b2a56f7c 126 return '# Contact email address: <%s>'%(self.addr)
3454dce4
SE
127
128class num:
129 def __init__(self,w):
b2a56f7c
SE
130 self.what=w[0]
131 self.n=string.atol(w[1])
3454dce4 132 def out(self):
b2a56f7c 133 return '%s %d;'%(self.what,self.n)
3454dce4
SE
134
135class address:
136 def __init__(self,w):
137 self.w=w
b2a56f7c
SE
138 self.adr=w[1]
139 self.port=string.atoi(w[2])
140 if (self.port<1 or self.port>65535):
141 complain("invalid port number")
3454dce4 142 def out(self):
b2a56f7c 143 return 'address "%s"; port %d;'%(self.adr,self.port)
3454dce4
SE
144
145class rsakey:
146 def __init__(self,w):
b2a56f7c
SE
147 self.l=string.atoi(w[1])
148 self.e=w[2]
149 self.n=w[3]
3454dce4 150 def out(self):
b2a56f7c 151 return 'key rsa-public("%s","%s");'%(self.e,self.n)
3454dce4
SE
152
153class mobileoption:
154 def __init__(self,w):
155 self.w=w
156 def out(self):
08f344d3 157 return '# netlink-options "soft";'
3454dce4
SE
158
159def complain(msg):
160 global complaints
161 print ("%s line %d: "%(file,line))+msg
162 complaints=complaints+1
163def moan(msg):
164 global complaints
165 print msg;
166 complaints=complaints+1
167
168# We don't allow redefinition of properties (because that would allow things
169# like restrict-nets to be redefined, which would be bad)
170def set(obj,defs,w):
171 if (obj.allow_defs | allow_defs):
172 if (obj.defs.has_key(w[0])):
173 complain("%s is already defined"%(w[0]))
174 else:
175 t=defs[w[0]]
176 obj.defs[w[0]]=t(w)
177
178# Process a line of configuration file
179def pline(i):
180 global allow_defs, group, current_vpn, current_location, current_object
181 w=string.split(i)
182 if len(w)==0: return
183 keyword=w[0]
184 if keyword=='end-definitions':
185 allow_defs=0
186 current_vpn=None
187 current_location=None
188 current_object=None
189 return
190 if keyword=='vpn':
191 if vpns.has_key(w[1]):
192 current_vpn=vpns[w[1]]
193 current_object=current_vpn
194 else:
195 if allow_defs:
196 current_vpn=vpn(w[1])
197 vpns[w[1]]=current_vpn
198 current_object=current_vpn
199 else:
200 complain("no new VPN definitions allowed")
201 return
202 if (current_vpn==None):
203 complain("no VPN defined yet")
204 return
205 # Keywords that can apply at all levels
206 if mldefs.has_key(w[0]):
207 set(current_object,mldefs,w)
208 return
209 if keyword=='location':
210 if (current_vpn.locations.has_key(w[1])):
211 current_location=current_vpn.locations[w[1]]
212 current_object=current_location
213 if (group and not allow_defs and
214 current_location.group!=group):
215 complain(("must be group %s to access "+
216 "location %s")%(current_location.group,
217 w[1]))
218 else:
219 if allow_defs:
220 if reserved.has_key(w[1]):
221 complain("reserved location name")
222 return
223 current_location=location(w[1],current_vpn)
224 current_vpn.locations[w[1]]=current_location
225 current_object=current_location
226 else:
227 complain("no new location definitions allowed")
228 return
229 if (current_location==None):
230 complain("no locations defined yet")
231 return
232 if keyword=='group':
233 current_location.group=w[1]
234 return
235 if keyword=='site':
236 if (current_location.sites.has_key(w[1])):
237 current_object=current_location.sites[w[1]]
238 else:
239 if reserved.has_key(w[1]):
240 complain("reserved site name")
241 return
242 current_object=site(w[1],current_location)
243 current_location.sites[w[1]]=current_object
244 return
245 if keyword=='endsite':
246 if isinstance(current_object,site):
247 current_object=current_object.location
248 else:
249 complain("not currently defining a site")
250 return
251 # Keywords that can only apply to sites
252 if isinstance(current_object,site):
253 if sitedefs.has_key(w[0]):
254 set(current_object,sitedefs,w)
255 return
256 else:
257 if sitedefs.has_key(w[0]):
258 complain("keyword '%s' can only be used in the "
259 "context of a site definition"%(w[0]))
260 return
261 complain("unknown keyword '%s'"%(w[0]))
262
263def pfile(name,lines):
264 global file,line
265 file=name
266 line=0
267 for i in lines:
268 line=line+1
269 if (i[0]=='#'): continue
270 if (i[len(i)-1]=='\n'): i=i[:len(i)-1] # strip trailing LF
271 pline(i)
272
273def outputsites(w):
274 w.write("# secnet sites file autogenerated by make-secnet-sites.py "
275 +"version %s\n"%VERSION)
276 w.write("# %s\n\n"%time.asctime(time.localtime(time.time())))
277
278 # Raw VPN data section of file
279 w.write("vpn-data {\n")
280 for i in vpns.values():
281 w.write(" %s {\n"%i.name)
282 for d in i.defs.values():
283 w.write(" %s\n"%d.out())
284 w.write("\n")
285 for l in i.locations.values():
286 w.write(" %s {\n"%l.name)
287 for d in l.defs.values():
288 w.write(" %s\n"%d.out())
289 for s in l.sites.values():
290 w.write(" %s {\n"%s.name)
291 w.write(' name "%s/%s/%s";\n'%
292 (i.name,l.name,s.name))
293 for d in s.defs.values():
294 w.write(" %s\n"%d.out())
295 w.write(" };\n")
296 w.write(" };\n")
297 w.write(" };\n")
298 w.write("};\n")
299
300 # Per-VPN flattened lists
301 w.write("vpn {\n")
302 for i in vpns.values():
303 w.write(" %s {\n"%(i.name))
304 for l in i.locations.values():
b2a56f7c
SE
305 tmpl="vpn-data/%s/%s/%%s"%(i.name,l.name)
306 slist=[]
307 for s in l.sites.values(): slist.append(tmpl%s.name)
3454dce4
SE
308 w.write(" %s %s;\n"%(l.name,string.join(slist,",")))
309 w.write("\n all-sites %s;\n"%
310 string.join(i.locations.keys(),","))
311 w.write(" };\n")
312 w.write("};\n")
313
314 # Flattened list of sites
315 w.write("all-sites %s;\n"%string.join(map(lambda x:"vpn/%s/all-sites"%
316 x,vpns.keys()),","))
317
318# Are we being invoked from userv?
319service=0
320# If we are, which group does the caller want to modify?
321group=None
322
323vpns={}
324allow_defs=1
325current_vpn=None
326current_location=None
327current_object=None
328
329line=0
330file=None
331complaints=0
332
333# Things that can be defined at any level
334mldefs={
335 'dh':dhgroup,
336 'hash':hash,
337 'contact':email,
338 'key-lifetime':num,
339 'setup-retries':num,
340 'setup-timeout':num,
341 'wait-time':num,
342 'renegotiate-time':num,
343 'restrict-nets':nets
344 }
345
346# Things that can only be defined for sites
347sitedefs={
348 'address':address,
349 'networks':nets,
350 'pubkey':rsakey,
351 'mobile':mobileoption
352 }
353
354# Reserved vpn/location/site names
355reserved={'all-sites':None}
356reserved.update(mldefs)
357reserved.update(sitedefs)
358
359# Each site must have the following defined at some level:
360required={
361 'dh':"Diffie-Hellman group",
362 'networks':"network list",
363 'pubkey':"public key",
364 'hash':"hash function"
365 }
366
367if len(sys.argv)<2:
368 pfile("stdin",sys.stdin.readlines())
369 of=sys.stdout
370else:
371 if sys.argv[1]=='-u':
372 if len(sys.argv)!=6:
373 print "Wrong number of arguments"
374 sys.exit(1)
375 service=1
376 header=sys.argv[2]
377 groupfiledir=sys.argv[3]
378 sitesfile=sys.argv[4]
379 group=sys.argv[5]
380 if not os.environ.has_key("USERV_USER"):
381 print "Environment variable USERV_USER not found"
382 sys.exit(1)
383 user=os.environ["USERV_USER"]
384 # Check that group is in USERV_GROUP
385 if not os.environ.has_key("USERV_GROUP"):
386 print "Environment variable USERV_GROUP not found"
387 sys.exit(1)
388 ugs=os.environ["USERV_GROUP"]
389 ok=0
390 for i in string.split(ugs):
391 if group==i: ok=1
392 if not ok:
393 print "caller not in group %s"%group
394 sys.exit(1)
395 f=open(header)
08f344d3 396 headerinput=f.readlines()
3454dce4 397 f.close()
08f344d3 398 pfile(header,headerinput)
3454dce4
SE
399 userinput=sys.stdin.readlines()
400 pfile("user input",userinput)
401 else:
402 if len(sys.argv)>3:
403 print "Too many arguments"
404 sys.exit(1)
405 f=open(sys.argv[1])
406 pfile(sys.argv[1],f.readlines())
407 f.close()
408 of=sys.stdout
409 if len(sys.argv)>2:
410 of=open(sys.argv[2],'w')
411
412# Sanity check section
413
414# Delete locations that have no sites defined
415for i in vpns.values():
416 for l in i.locations.keys():
417 if (len(i.locations[l].sites.values())==0):
418 del i.locations[l]
419
420# Delete VPNs that have no locations with sites defined
421for i in vpns.keys():
422 if (len(vpns[i].locations.values())==0):
423 del vpns[i]
424
425# Check all sites
426for i in vpns.values():
427 if i.defs.has_key('restrict-nets'):
428 vr=i.defs['restrict-nets']
429 else:
430 vr=None
431 for l in i.locations.values():
432 if l.defs.has_key('restrict-nets'):
433 lr=l.defs['restrict-nets']
434 if (not lr.subsetof(vr)):
435 moan("location %s/%s restrict-nets is invalid"%
436 (i.name,l.name))
437 else:
438 lr=vr
439 for s in l.sites.values():
440 sn="%s/%s/%s"%(i.name,l.name,s.name)
441 for r in required.keys():
442 if (not (s.defs.has_key(r) or
443 l.defs.has_key(r) or
444 i.defs.has_key(r))):
445 moan("site %s missing parameter %s"%
446 (sn,r))
447 if s.defs.has_key('restrict-nets'):
448 sr=s.defs['restrict-nets']
449 if (not sr.subsetof(lr)):
450 moan("site %s restrict-nets not valid"%
451 sn)
452 else:
453 sr=lr
454 if not s.defs.has_key('networks'): continue
455 nets=s.defs['networks']
456 if (not nets.subsetof(sr)):
457 moan("site %s networks exceed restriction"%sn)
458
459
460if complaints>0:
461 if complaints==1: print "There was 1 problem."
462 else: print "There were %d problems."%(complaints)
463 sys.exit(1)
464
465if service:
466 # Put the user's input into their group file, and rebuild the main
467 # sites file
08f344d3 468 f=open(groupfiledir+"/T"+group,'w')
3454dce4
SE
469 f.write("# Section submitted by user %s, %s\n"%
470 (user,time.asctime(time.localtime(time.time()))))
471 f.write("# Checked by make-secnet-sites.py version %s\n\n"%VERSION)
472 for i in userinput: f.write(i)
473 f.write("\n")
474 f.close()
08f344d3
SE
475 os.rename(groupfiledir+"/T"+group,groupfiledir+"/R"+group)
476 f=open(sitesfile+"-tmp",'w')
477 f.write("# sites file autogenerated by make-secnet-sites.py\n")
478 f.write("# generated %s, invoked by %s\n"%
479 (time.asctime(time.localtime(time.time())),user))
480 f.write("# use make-secnet-sites.py to turn this file into a\n")
481 f.write("# valid /etc/secnet/sites.conf file\n\n")
482 for i in headerinput: f.write(i)
483 files=os.listdir(groupfiledir)
484 for i in files:
485 if i[0]=='R':
486 j=open(groupfiledir+"/"+i)
487 f.write(j.read())
488 j.close()
489 f.write("# end of sites file\n")
490 f.close()
491 os.rename(sitesfile+"-tmp",sitesfile)
3454dce4
SE
492else:
493 outputsites(of)