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
57 sys
.path
.append("/usr/local/share/secnet")
58 sys
.path
.append("/usr/share/secnet")
64 def __init__(self
,name
):
71 def __init__(self
,name
,vpn
):
80 def __init__(self
,name
,location
):
83 self
.location
=location
89 self
.set=ipaddr
.ip_set()
92 self
.set.append(ipaddr
.network(x
[0],x
[1],
93 ipaddr
.DEMAND_NETWORK
))
95 # I'd like to do this:
96 # return self.set.is_subset(s)
97 # but there isn't an is_subset() method
98 # Instead we see if we intersect with the complement of s
100 i
=sc
.intersection(self
.set)
103 if (self
.w
[0]=='restrict-nets'): pattern
="# restrict-nets %s;"
105 pattern
="link netlink { routes %s; };"
106 return pattern%string
.join(map(lambda x
:'"%s/%s"'%(x
.ip_str(),
107 x
.mask
.netmask_bits_str
),
108 self
.set.as_list_of_networks()),",")
111 def __init__(self
,w
):
115 return 'dh diffie-hellman("%s","%s");'%(self
.mod
,self
.gen
)
118 def __init__(self
,w
):
120 if (self
.ht
!='md5' and self
.ht
!='sha1'):
121 complain("unknown hash type %s"%(self
.ht
))
123 return 'hash %s;'%(self
.ht
)
126 def __init__(self
,w
):
129 return '# Contact email address: <%s>'%(self
.addr
)
132 def __init__(self
,w
):
134 self
.n
=string
.atol(w
[1])
136 return '%s %d;'%(self
.what
,self
.n
)
139 def __init__(self
,w
):
142 self
.port
=string
.atoi(w
[2])
143 if (self
.port
<1 or self
.port
>65535):
144 complain("invalid port number")
146 return 'address "%s"; port %d;'%(self
.adr
,self
.port
)
149 def __init__(self
,w
):
150 self
.l
=string
.atoi(w
[1])
154 return 'key rsa-public("%s","%s");'%(self
.e
,self
.n
)
157 def __init__(self
,w
):
160 return '# netlink-options "soft";'
164 print ("%s line %d: "%(file,line
))+msg
165 complaints
=complaints
+1
169 complaints
=complaints
+1
171 # We don't allow redefinition of properties (because that would allow things
172 # like restrict-nets to be redefined, which would be bad)
174 if (obj
.allow_defs | allow_defs
):
175 if (obj
.defs
.has_key(w
[0])):
176 complain("%s is already defined"%(w
[0]))
181 # Process a line of configuration file
183 global allow_defs
, group
, current_vpn
, current_location
, current_object
187 if keyword
=='end-definitions':
190 current_location
=None
194 if vpns
.has_key(w
[1]):
195 current_vpn
=vpns
[w
[1]]
196 current_object
=current_vpn
199 current_vpn
=vpn(w
[1])
200 vpns
[w
[1]]=current_vpn
201 current_object
=current_vpn
203 complain("no new VPN definitions allowed")
205 if (current_vpn
==None):
206 complain("no VPN defined yet")
208 # Keywords that can apply at all levels
209 if mldefs
.has_key(w
[0]):
210 set(current_object
,mldefs
,w
)
212 if keyword
=='location':
213 if (current_vpn
.locations
.has_key(w
[1])):
214 current_location
=current_vpn
.locations
[w
[1]]
215 current_object
=current_location
216 if (group
and not allow_defs
and
217 current_location
.group
!=group
):
218 complain(("must be group %s to access "+
219 "location %s")%(current_location
.group
,
223 if reserved
.has_key(w
[1]):
224 complain("reserved location name")
226 current_location
=location(w
[1],current_vpn
)
227 current_vpn
.locations
[w
[1]]=current_location
228 current_object
=current_location
230 complain("no new location definitions allowed")
232 if (current_location
==None):
233 complain("no locations defined yet")
236 current_location
.group
=w
[1]
239 if (current_location
.sites
.has_key(w
[1])):
240 current_object
=current_location
.sites
[w
[1]]
242 if reserved
.has_key(w
[1]):
243 complain("reserved site name")
245 current_object
=site(w
[1],current_location
)
246 current_location
.sites
[w
[1]]=current_object
248 if keyword
=='endsite':
249 if isinstance(current_object
,site
):
250 current_object
=current_object
.location
252 complain("not currently defining a site")
254 # Keywords that can only apply to sites
255 if isinstance(current_object
,site
):
256 if sitedefs
.has_key(w
[0]):
257 set(current_object
,sitedefs
,w
)
260 if sitedefs
.has_key(w
[0]):
261 complain("keyword '%s' can only be used in the "
262 "context of a site definition"%(w
[0]))
264 complain("unknown keyword '%s'"%(w
[0]))
266 def pfile(name
,lines
):
272 if (i
[0]=='#'): continue
273 if (i
[len(i
)-1]=='\n'): i
=i
[:len(i
)-1] # strip trailing LF
277 w
.write("# secnet sites file autogenerated by make-secnet-sites.py "
278 +"version %s\n"%VERSION
)
279 w
.write("# %s\n\n"%time
.asctime(time
.localtime(time
.time())))
281 # Raw VPN data section of file
282 w
.write("vpn-data {\n")
283 for i
in vpns
.values():
284 w
.write(" %s {\n"%i
.name
)
285 for d
in i
.defs
.values():
286 w
.write(" %s\n"%d
.out())
288 for l
in i
.locations
.values():
289 w
.write(" %s {\n"%l
.name
)
290 for d
in l
.defs
.values():
291 w
.write(" %s\n"%d
.out())
292 for s
in l
.sites
.values():
293 w
.write(" %s {\n"%s
.name
)
294 w
.write(' name "%s/%s/%s";\n'%
295 (i
.name
,l
.name
,s
.name
))
296 for d
in s
.defs
.values():
297 w
.write(" %s\n"%d
.out())
303 # Per-VPN flattened lists
305 for i
in vpns
.values():
306 w
.write(" %s {\n"%(i
.name
))
307 for l
in i
.locations
.values():
308 tmpl
="vpn-data/%s/%s/%%s"%(i
.name
,l
.name
)
310 for s
in l
.sites
.values(): slist
.append(tmpl%s
.name
)
311 w
.write(" %s %s;\n"%(l
.name
,string
.join(slist
,",")))
312 w
.write("\n all-sites %s;\n"%
313 string
.join(i
.locations
.keys(),","))
317 # Flattened list of sites
318 w
.write("all-sites %s;\n"%string
.join(map(lambda x
:"vpn/%s/all-sites"%
321 # Are we being invoked from userv?
323 # If we are, which group does the caller want to modify?
329 current_location
=None
336 # Things that can be defined at any level
345 'renegotiate-time':num
,
349 # Things that can only be defined for sites
354 'mobile':mobileoption
357 # Reserved vpn/location/site names
358 reserved
={'all-sites':None}
359 reserved
.update(mldefs
)
360 reserved
.update(sitedefs
)
362 # Each site must have the following defined at some level:
364 'dh':"Diffie-Hellman group",
365 'networks':"network list",
366 'pubkey':"public key",
367 'hash':"hash function"
371 pfile("stdin",sys
.stdin
.readlines())
374 if sys
.argv
[1]=='-u':
376 print "Wrong number of arguments"
380 groupfiledir
=sys
.argv
[3]
381 sitesfile
=sys
.argv
[4]
383 if not os
.environ
.has_key("USERV_USER"):
384 print "Environment variable USERV_USER not found"
386 user
=os
.environ
["USERV_USER"]
387 # Check that group is in USERV_GROUP
388 if not os
.environ
.has_key("USERV_GROUP"):
389 print "Environment variable USERV_GROUP not found"
391 ugs
=os
.environ
["USERV_GROUP"]
393 for i
in string
.split(ugs
):
396 print "caller not in group %s"%group
399 headerinput
=f
.readlines()
401 pfile(header
,headerinput
)
402 userinput
=sys
.stdin
.readlines()
403 pfile("user input",userinput
)
406 print "Too many arguments"
409 pfile(sys
.argv
[1],f
.readlines())
413 of
=open(sys
.argv
[2],'w')
415 # Sanity check section
417 # Delete locations that have no sites defined
418 for i
in vpns
.values():
419 for l
in i
.locations
.keys():
420 if (len(i
.locations
[l
].sites
.values())==0):
423 # Delete VPNs that have no locations with sites defined
424 for i
in vpns
.keys():
425 if (len(vpns
[i
].locations
.values())==0):
429 for i
in vpns
.values():
430 if i
.defs
.has_key('restrict-nets'):
431 vr
=i
.defs
['restrict-nets']
434 for l
in i
.locations
.values():
435 if l
.defs
.has_key('restrict-nets'):
436 lr
=l
.defs
['restrict-nets']
437 if (not lr
.subsetof(vr
)):
438 moan("location %s/%s restrict-nets is invalid"%
442 for s
in l
.sites
.values():
443 sn
="%s/%s/%s"%(i
.name
,l
.name
,s
.name
)
444 for r
in required
.keys():
445 if (not (s
.defs
.has_key(r
) or
448 moan("site %s missing parameter %s"%
450 if s
.defs
.has_key('restrict-nets'):
451 sr
=s
.defs
['restrict-nets']
452 if (not sr
.subsetof(lr
)):
453 moan("site %s restrict-nets not valid"%
457 if not s
.defs
.has_key('networks'): continue
458 nets
=s
.defs
['networks']
459 if (not nets
.subsetof(sr
)):
460 moan("site %s networks exceed restriction"%sn
)
464 if complaints
==1: print "There was 1 problem."
465 else: print "There were %d problems."%(complaints)
469 # Put the user's input into their group file, and rebuild the main
471 f
=open(groupfiledir
+"/T"+group
,'w')
472 f
.write("# Section submitted by user %s, %s\n"%
473 (user
,time
.asctime(time
.localtime(time
.time()))))
474 f
.write("# Checked by make-secnet-sites.py version %s\n\n"%VERSION
)
475 for i
in userinput
: f
.write(i
)
478 os
.rename(groupfiledir
+"/T"+group
,groupfiledir
+"/R"+group
)
479 f
=open(sitesfile
+"-tmp",'w')
480 f
.write("# sites file autogenerated by make-secnet-sites\n")
481 f
.write("# generated %s, invoked by %s\n"%
482 (time
.asctime(time
.localtime(time
.time())),user
))
483 f
.write("# use make-secnet-sites to turn this file into a\n")
484 f
.write("# valid /etc/secnet/sites.conf file\n\n")
485 for i
in headerinput
: f
.write(i
)
486 files
=os
.listdir(groupfiledir
)
489 j
=open(groupfiledir
+"/"+i
)
492 f
.write("# end of sites file\n")
494 os
.rename(sitesfile
+"-tmp",sitesfile
)