2 # Copyright (C) 2001 Stephen Early <steve@greenend.org.uk>
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.
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.
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
18 """VPN sites file manipulation.
20 This program enables VPN site descriptions to be submitted for
21 inclusion in a central database, and allows the resulting database to
22 be turned into a secnet configuration file.
24 A database file can be turned into a secnet configuration file simply:
25 make-secnet-sites.py [infile [outfile]]
27 It would be wise to run secnet with the "--just-check-config" option
28 before installing the output on a live system.
30 The program expects to be invoked via userv to manage the database; it
31 relies on the USERV_USER and USERV_GROUP environment variables. The
32 command line arguments for this invocation are:
34 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37 All but the last argument are expected to be set by userv; the 'group'
38 argument is provided by the user. A suitable userv configuration file
44 cd ~/secnet/sites-test/
45 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
47 This program is part of secnet. It relies on the "ipaddr" library from
61 def __init__(self,name):
68 def __init__(self,name,vpn):
77 def __init__(self,name,location):
80 self.location=location
86 self.set=ipaddr.ip_set()
89 self.set.append(ipaddr.network(x[0],x[1],
90 ipaddr.DEMAND_NETWORK))
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
97 i=sc.intersection(self.set)
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()),","))
108 def __init__(self,w):
112 return 'dh diffie-hellman("%s","%s");'%(self.mod,self.gen)
115 def __init__(self,w):
117 if (self.ht!='md5' and self.ht!='sha1'):
118 complain("unknown hash type %s"%(self.ht))
120 return 'hash %s;'%(self.ht)
123 def __init__(self,w):
126 return '# Contact email address: <%s>'%(self.addr)
129 def __init__(self,w):
131 self.n=string.atol(w[1])
133 return '%s %d;'%(self.what,self.n)
136 def __init__(self,w):
139 self.port=string.atoi(w[2])
140 if (self.port<1 or self.port>65535):
141 complain("invalid port number")
143 return 'address "%s"; port %d;'%(self.adr,self.port)
146 def __init__(self,w):
147 self.l=string.atoi(w[1])
151 return 'key rsa-public("%s","%s");'%(self.e,self.n)
154 def __init__(self,w):
157 return '# netlink-options "soft";'
161 print ("%s line %d: "%(file,line))+msg
162 complaints=complaints+1
166 complaints=complaints+1
168 # We don't allow redefinition of properties (because that would allow things
169 # like restrict-nets to be redefined, which would be bad)
171 if (obj.allow_defs | allow_defs):
172 if (obj.defs.has_key(w[0])):
173 complain("%s is already defined"%(w[0]))
178 # Process a line of configuration file
180 global allow_defs, group, current_vpn, current_location, current_object
184 if keyword=='end-definitions':
187 current_location=None
191 if vpns.has_key(w[1]):
192 current_vpn=vpns[w[1]]
193 current_object=current_vpn
196 current_vpn=vpn(w[1])
197 vpns[w[1]]=current_vpn
198 current_object=current_vpn
200 complain("no new VPN definitions allowed")
202 if (current_vpn==None):
203 complain("no VPN defined yet")
205 # Keywords that can apply at all levels
206 if mldefs.has_key(w[0]):
207 set(current_object,mldefs,w)
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,
220 if reserved.has_key(w[1]):
221 complain("reserved location name")
223 current_location=location(w[1],current_vpn)
224 current_vpn.locations[w[1]]=current_location
225 current_object=current_location
227 complain("no new location definitions allowed")
229 if (current_location==None):
230 complain("no locations defined yet")
233 current_location.group=w[1]
236 if (current_location.sites.has_key(w[1])):
237 current_object=current_location.sites[w[1]]
239 if reserved.has_key(w[1]):
240 complain("reserved site name")
242 current_object=site(w[1],current_location)
243 current_location.sites[w[1]]=current_object
245 if keyword=='endsite':
246 if isinstance(current_object,site):
247 current_object=current_object.location
249 complain("not currently defining a site")
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)
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]))
261 complain("unknown keyword '%s'"%(w[0]))
263 def pfile(name,lines):
269 if (i[0]=='#'): continue
270 if (i[len(i)-1]=='\n'): i=i[:len(i)-1] # strip trailing LF
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())))
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())
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())
300 # Per-VPN flattened lists
302 for i in vpns.values():
303 w.write(" %s {\n"%(i.name))
304 for l in i.locations.values():
305 tmpl="vpn-data/%s/%s/%%s"%(i.name,l.name)
307 for s in l.sites.values(): slist.append(tmpl%s.name)
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(),","))
314 # Flattened list of sites
315 w.write("all-sites %s;\n"%string.join(map(lambda x:"vpn/%s/all-sites"%
318 # Are we being invoked from userv?
320 # If we are, which group does the caller want to modify?
326 current_location=None
333 # Things that can be defined at any level
342 'renegotiate-time':num,
346 # Things that can only be defined for sites
351 'mobile':mobileoption
354 # Reserved vpn/location/site names
355 reserved={'all-sites':None}
356 reserved.update(mldefs)
357 reserved.update(sitedefs)
359 # Each site must have the following defined at some level:
361 'dh':"Diffie-Hellman group",
362 'networks':"network list",
363 'pubkey':"public key",
364 'hash':"hash function"
368 pfile("stdin",sys.stdin.readlines())
371 if sys.argv[1]=='-u':
373 print "Wrong number of arguments"
377 groupfiledir=sys.argv[3]
378 sitesfile=sys.argv[4]
380 if not os.environ.has_key("USERV_USER"):
381 print "Environment variable USERV_USER not found"
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"
388 ugs=os.environ["USERV_GROUP"]
390 for i in string.split(ugs):
393 print "caller not in group %s"%group
396 headerinput=f.readlines()
398 pfile(header,headerinput)
399 userinput=sys.stdin.readlines()
400 pfile("user input",userinput)
403 print "Too many arguments"
406 pfile(sys.argv[1],f.readlines())
410 of=open(sys.argv[2],'w')
412 # Sanity check section
414 # Delete locations that have no sites defined
415 for i in vpns.values():
416 for l in i.locations.keys():
417 if (len(i.locations[l].sites.values())==0):
420 # Delete VPNs that have no locations with sites defined
421 for i in vpns.keys():
422 if (len(vpns[i].locations.values())==0):
426 for i in vpns.values():
427 if i.defs.has_key('restrict-nets'):
428 vr=i.defs['restrict-nets']
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"%
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
445 moan("site %s missing parameter %s"%
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"%
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)
461 if complaints==1: print "There was 1 problem."
462 else: print "There were %d problems."%(complaints)
466 # Put the user's input into their group file, and rebuild the main
468 f=open(groupfiledir+"/T"+group,'w')
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)
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)
486 j=open(groupfiledir+"/"+i)
489 f.write("# end of sites file\n")
491 os.rename(sitesfile+"-tmp",sitesfile)