Import release 0.1.3
[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/
45execute ~/secnet/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
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
58VERSION="0.1.3"
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):
149 return 'netlink-options "soft";'
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)
388 pfile(header,f.readlines())
389 f.close()
390 userinput=sys.stdin.readlines()
391 pfile("user input",userinput)
392 else:
393 if len(sys.argv)>3:
394 print "Too many arguments"
395 sys.exit(1)
396 f=open(sys.argv[1])
397 pfile(sys.argv[1],f.readlines())
398 f.close()
399 of=sys.stdout
400 if len(sys.argv)>2:
401 of=open(sys.argv[2],'w')
402
403# Sanity check section
404
405# Delete locations that have no sites defined
406for i in vpns.values():
407 for l in i.locations.keys():
408 if (len(i.locations[l].sites.values())==0):
409 del i.locations[l]
410
411# Delete VPNs that have no locations with sites defined
412for i in vpns.keys():
413 if (len(vpns[i].locations.values())==0):
414 del vpns[i]
415
416# Check all sites
417for i in vpns.values():
418 if i.defs.has_key('restrict-nets'):
419 vr=i.defs['restrict-nets']
420 else:
421 vr=None
422 for l in i.locations.values():
423 if l.defs.has_key('restrict-nets'):
424 lr=l.defs['restrict-nets']
425 if (not lr.subsetof(vr)):
426 moan("location %s/%s restrict-nets is invalid"%
427 (i.name,l.name))
428 else:
429 lr=vr
430 for s in l.sites.values():
431 sn="%s/%s/%s"%(i.name,l.name,s.name)
432 for r in required.keys():
433 if (not (s.defs.has_key(r) or
434 l.defs.has_key(r) or
435 i.defs.has_key(r))):
436 moan("site %s missing parameter %s"%
437 (sn,r))
438 if s.defs.has_key('restrict-nets'):
439 sr=s.defs['restrict-nets']
440 if (not sr.subsetof(lr)):
441 moan("site %s restrict-nets not valid"%
442 sn)
443 else:
444 sr=lr
445 if not s.defs.has_key('networks'): continue
446 nets=s.defs['networks']
447 if (not nets.subsetof(sr)):
448 moan("site %s networks exceed restriction"%sn)
449
450
451if complaints>0:
452 if complaints==1: print "There was 1 problem."
453 else: print "There were %d problems."%(complaints)
454 sys.exit(1)
455
456if service:
457 # Put the user's input into their group file, and rebuild the main
458 # sites file
459 f=open(groupfiledir+"-tmp/"+group,'w')
460 f.write("# Section submitted by user %s, %s\n"%
461 (user,time.asctime(time.localtime(time.time()))))
462 f.write("# Checked by make-secnet-sites.py version %s\n\n"%VERSION)
463 for i in userinput: f.write(i)
464 f.write("\n")
465 f.close()
466 os.rename(groupfiledir+"-tmp/"+group,groupfiledir+"/"+group)
467 # XXX rebuild main sites file!
468else:
469 outputsites(of)