2 # Copyright (C) 2001-2002 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 sys
.path
.insert(0,"/usr/local/share/secnet")
62 sys
.path
.insert(0,"/usr/share/secnet")
67 # Classes describing possible datatypes in the configuration file
72 self
.addr
=ipaddr
.IPAddress(w
[1])
74 return '"%s"'%self
.addr
77 "A set of IP addresses specified as a list of networks"
79 self
.set=ipaddrset
.IPAddressSet()
81 x
=ipaddr
.IPNetwork(i
,strict
=True)
84 return ",".join(map((lambda n
: '"%s"'%n
), self
.set.networks()))
87 "A Diffie-Hellman group"
92 return 'diffie-hellman("%s","%s")'%(self
.mod
,self
.gen
)
95 "A choice of hash function"
98 if (self
.ht
!='md5' and self
.ht
!='sha1'):
99 complain("unknown hash type %s"%(self
.ht
))
101 return '%s'%(self
.ht
)
105 def __init__(self
,w
):
108 return '<%s>'%(self
.addr
)
112 def __init__(self
,w
):
113 if re
.match('[TtYy1]',w
[1]):
115 elif re
.match('[FfNn0]',w
[1]):
118 complain("invalid boolean value");
120 return ['False','True'][self
.b
]
124 def __init__(self
,w
):
125 self
.n
=string
.atol(w
[1])
130 "A DNS name and UDP port number"
131 def __init__(self
,w
):
133 self
.port
=string
.atoi(w
[2])
134 if (self
.port
<1 or self
.port
>65535):
135 complain("invalid port number")
137 return '"%s"; port %d'%(self
.adr
,self
.port
)
141 def __init__(self
,w
):
142 self
.l
=string
.atoi(w
[1])
146 return 'rsa-public("%s","%s")'%(self
.e
,self
.n
)
148 # Possible properties of configuration nodes
150 'contact':(email
,"Contact address"),
151 'dh':(dhgroup
,"Diffie-Hellman group"),
152 'hash':(hash,"Hash function"),
153 'key-lifetime':(num
,"Maximum key lifetime (ms)"),
154 'setup-timeout':(num
,"Key setup timeout (ms)"),
155 'setup-retries':(num
,"Maximum key setup packet retries"),
156 'wait-time':(num
,"Time to wait after unsuccessful key setup (ms)"),
157 'renegotiate-time':(num
,"Time after key setup to begin renegotiation (ms)"),
158 'restrict-nets':(networks
,"Allowable networks"),
159 'networks':(networks
,"Claimed networks"),
160 'pubkey':(rsakey
,"RSA public site key"),
161 'peer':(single_ipaddr
,"Tunnel peer IP address"),
162 'address':(address
,"External contact address and port"),
163 'mobile':(boolean
,"Site is mobile"),
167 "Simply output a property - the default case"
168 return "%s %s;\n"%(name
,value
)
170 # All levels support these properties
172 'contact':(lambda name
,value
:"# Contact email address: %s\n"%(value)
),
179 'renegotiate-time':sp
,
180 'restrict-nets':(lambda name
,value
:"# restrict-nets %s\n"%value
),
184 "A level in the configuration hierarchy"
188 require_properties
={}
189 def __init__(self
,w
):
193 def indent(self
,w
,t
):
195 def prop_out(self
,n
):
196 return self
.allow_properties
[n
](n
,str(self
.properties
[n
]))
197 def output_props(self
,w
,ind
):
198 for i
in self
.properties
.keys():
199 if self
.allow_properties
[i
]:
201 w
.write("%s"%self
.prop_out(i
))
202 def output_data(self
,w
,ind
,np
):
204 w
.write("%s {\n"%(self
.name
))
205 self
.output_props(w
,ind
+2)
206 if self
.depth
==1: w
.write("\n");
207 for c
in self
.children
.values():
208 c
.output_data(w
,ind
+2,np
+self
.name
+"/")
212 class vpnlevel(level
):
213 "VPN level in the configuration hierarchy"
217 allow_properties
=global_properties
.copy()
219 'contact':"VPN admin contact address"
221 def __init__(self
,w
):
222 level
.__init__(self
,w
)
223 def output_vpnflat(self
,w
,ind
,h
):
224 "Output flattened list of site names for this VPN"
226 w
.write("%s {\n"%(self
.name
))
227 for i
in self
.children
.keys():
228 self
.children
[i
].output_vpnflat(w
,ind
+2,
229 h
+"/"+self
.name
+"/"+i
)
232 w
.write("all-sites %s;\n"%
233 string
.join(self
.children
.keys(),','))
237 class locationlevel(level
):
238 "Location level in the configuration hierarchy"
242 allow_properties
=global_properties
.copy()
244 'contact':"Location admin contact address",
246 def __init__(self
,w
):
247 level
.__init__(self
,w
)
249 def output_vpnflat(self
,w
,ind
,h
):
251 # The "h=h,self=self" abomination below exists because
252 # Python didn't support nested_scopes until version 2.1
253 w
.write("%s %s;\n"%(self
.name
,string
.join(
254 map(lambda x
,h
=h
,self
=self
:
255 h
+"/"+x
,self
.children
.keys()),',')))
257 class sitelevel(level
):
258 "Site level (i.e. a leafnode) in the configuration hierarchy"
262 allow_properties
=global_properties
.copy()
263 allow_properties
.update({
267 'pubkey':(lambda n
,v
:"key %s;\n"%v
),
268 'address':(lambda n
,v
:"address %s;\n"%v
),
272 'dh':"Diffie-Hellman group",
273 'contact':"Site admin contact address",
274 'networks':"Networks claimed by the site",
275 'hash':"hash function",
276 'peer':"Gateway address of the site",
277 'pubkey':"RSA public key of the site",
279 def __init__(self
,w
):
280 level
.__init__(self
,w
)
281 def output_data(self
,w
,ind
,np
):
283 w
.write("%s {\n"%(self
.name
))
285 w
.write("name \"%s\";\n"%(np
+self
.name
))
286 self
.output_props(w
,ind
+2)
288 w
.write("link netlink {\n");
290 w
.write("routes %s;\n"%str
(self
.properties
["networks"]))
292 w
.write("ptp-address %s;\n"%str
(self
.properties
["peer"]))
298 # Levels in the configuration file
300 levels
={'vpn':vpnlevel
, 'location':locationlevel
, 'site':sitelevel
}
302 # Reserved vpn/location/site names
303 reserved
={'all-sites':None}
304 reserved
.update(keywords
)
305 reserved
.update(levels
)
308 "Complain about a particular input line"
310 print ("%s line %d: "%(file,line
))+msg
311 complaints
=complaints
+1
313 "Complain about something in general"
316 complaints
=complaints
+1
318 root
=level(['root','root']) # All vpns are children of this node
320 allow_defs
=0 # Level above which new definitions are permitted
323 def set_property(obj
,w
):
324 "Set a property on a configuration node"
325 if obj
.properties
.has_key(w
[0]):
326 complain("%s %s already has property %s defined"%
327 (obj
.type,obj
.name
,w
[0]))
329 obj
.properties
[w
[0]]=keywords
[w
[0]][0](w
)
331 def pline(i
,allow_include
=False):
332 "Process a configuration file line"
333 global allow_defs
, obstack
, root
334 w
=string
.split(i
.rstrip('\n'))
335 if len(w
)==0: return [i
]
337 current
=obstack
[len(obstack
)-1]
338 if keyword
=='end-definitions':
339 allow_defs
=sitelevel
.depth
342 if keyword
=='include':
343 if not allow_include
:
344 complain("include not permitted here")
347 complain("include requires one argument")
349 newfile
=os
.path
.join(os
.path
.dirname(file),w
[1])
350 return pfilepath(newfile
,allow_include
=allow_include
)
351 if levels
.has_key(keyword
):
352 # We may go up any number of levels, but only down by one
353 newdepth
=levels
[keyword
].depth
354 currentdepth
=len(obstack
) # actually +1...
355 if newdepth
<=currentdepth
:
356 obstack
=obstack
[:newdepth
]
357 if newdepth
>currentdepth
:
358 complain("May not go from level %d to level %d"%
359 (currentdepth
-1,newdepth
))
360 # See if it's a new one (and whether that's permitted)
362 current
=obstack
[len(obstack
)-1]
363 if current
.children
.has_key(w
[1]):
365 current
=current
.children
[w
[1]]
366 if service
and group
and current
.depth
==2:
367 if group
!=current
.group
:
368 complain("Incorrect group!")
371 # Ignore depth check for now
372 nl
=levels
[keyword
](w
)
373 if nl
.depth
<allow_defs
:
374 complain("New definitions not allowed at "
376 # we risk crashing if we continue
378 current
.children
[w
[1]]=nl
380 obstack
.append(current
)
382 if current
.allow_properties
.has_key(keyword
):
383 set_property(current
,w
)
386 complain("Property %s not allowed at %s level"%
387 (keyword
,current
.type))
390 complain("unknown keyword '%s'"%(keyword)
)
392 def pfilepath(pathname
,allow_include
=False):
394 outlines
=pfile(pathname
,f
.readlines(),allow_include
=allow_include
)
398 def pfile(name
,lines
,allow_include
=False):
406 if (i
[0]=='#'): continue
407 outlines
+= pline(i
,allow_include
=allow_include
)
411 "Output include file for secnet configuration"
412 w
.write("# secnet sites file autogenerated by make-secnet-sites "
413 +"version %s\n"%VERSION
)
414 w
.write("# %s\n"%time
.asctime(time
.localtime(time
.time())))
415 w
.write("# Command line: %s\n\n"%string
.join(sys
.argv
))
417 # Raw VPN data section of file
418 w
.write(prefix
+"vpn-data {\n")
419 for i
in root
.children
.values():
420 i
.output_data(w
,2,"")
423 # Per-VPN flattened lists
424 w
.write(prefix
+"vpn {\n")
425 for i
in root
.children
.values():
426 i
.output_vpnflat(w
,2,prefix
+"vpn-data")
429 # Flattened list of sites
430 w
.write(prefix
+"all-sites %s;\n"%string
.join(
431 map(lambda x
:"%svpn/%s/all-sites"%(prefix
,x
),
432 root
.children
.keys()),","))
434 # Are we being invoked from userv?
436 # If we are, which group does the caller want to modify?
444 pfile("stdin",sys
.stdin
.readlines())
447 if sys
.argv
[1]=='-u':
449 print "Wrong number of arguments"
453 groupfiledir
=sys
.argv
[3]
454 sitesfile
=sys
.argv
[4]
456 if not os
.environ
.has_key("USERV_USER"):
457 print "Environment variable USERV_USER not found"
459 user
=os
.environ
["USERV_USER"]
460 # Check that group is in USERV_GROUP
461 if not os
.environ
.has_key("USERV_GROUP"):
462 print "Environment variable USERV_GROUP not found"
464 ugs
=os
.environ
["USERV_GROUP"]
466 for i
in string
.split(ugs
):
469 print "caller not in group %s"%group
471 headerinput
=pfilepath(header
,allow_include
=True)
472 userinput
=sys
.stdin
.readlines()
473 pfile("user input",userinput
)
475 if sys
.argv
[1]=='-P':
479 print "Too many arguments"
481 pfilepath(sys
.argv
[1])
484 of
=open(sys
.argv
[2],'w')
486 # Sanity check section
487 # Delete nodes where leaf=0 that have no children
490 "Number of leafnodes below node n"
492 for i
in n
.children
.keys():
493 if live(n
.children
[i
]): return 1
496 "Delete nodes that have no leafnode children"
497 for i
in n
.children
.keys():
498 delempty(n
.children
[i
])
499 if not live(n
.children
[i
]):
503 # Check that all constraints are met (as far as I can tell
504 # restrict-nets/networks/peer are the only special cases)
506 def checkconstraints(n
,p
,ra
):
508 new_p
.update(n
.properties
)
509 for i
in n
.require_properties
.keys():
510 if not new_p
.has_key(i
):
511 moan("%s %s is missing property %s"%
513 for i
in new_p
.keys():
514 if not n
.allow_properties
.has_key(i
):
515 moan("%s %s has forbidden property %s"%
517 # Check address range restrictions
518 if n
.properties
.has_key("restrict-nets"):
519 new_ra
=ra
.intersection(n
.properties
["restrict-nets"].set)
522 if n
.properties
.has_key("networks"):
523 if not n
.properties
["networks"].set <= new_ra
:
524 moan("%s %s networks out of bounds"%(n
.type,n
.name
))
525 if n
.properties
.has_key("peer"):
526 if not n
.properties
["networks"].set.contains(
527 n
.properties
["peer"].addr
):
528 moan("%s %s peer not in networks"%(n
.type,n
.name
))
529 for i
in n
.children
.keys():
530 checkconstraints(n
.children
[i
],new_p
,new_ra
)
532 checkconstraints(root
,{},ipaddrset
.complete_set())
535 if complaints
==1: print "There was 1 problem."
536 else: print "There were %d problems."%(complaints)
540 # Put the user's input into their group file, and rebuild the main
542 f
=open(groupfiledir
+"/T"+group
,'w')
543 f
.write("# Section submitted by user %s, %s\n"%
544 (user
,time
.asctime(time
.localtime(time
.time()))))
545 f
.write("# Checked by make-secnet-sites version %s\n\n"%VERSION
)
546 for i
in userinput
: f
.write(i
)
549 os
.rename(groupfiledir
+"/T"+group
,groupfiledir
+"/R"+group
)
550 f
=open(sitesfile
+"-tmp",'w')
551 f
.write("# sites file autogenerated by make-secnet-sites\n")
552 f
.write("# generated %s, invoked by %s\n"%
553 (time
.asctime(time
.localtime(time
.time())),user
))
554 f
.write("# use make-secnet-sites to turn this file into a\n")
555 f
.write("# valid /etc/secnet/sites.conf file\n\n")
556 for i
in headerinput
: f
.write(i
)
557 files
=os
.listdir(groupfiledir
)
560 j
=open(groupfiledir
+"/"+i
)
563 f
.write("# end of sites file\n")
565 os
.rename(sitesfile
+"-tmp",sitesfile
)