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