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
58 # The ipaddr library is installed as part of secnet
59 sys
.path
.append("/usr/local/share/secnet")
60 sys
.path
.append("/usr/share/secnet")
65 # Classes describing possible datatypes in the configuration file
70 self
.addr
=ipaddr
.ipaddr(w
[1])
72 return '"%s"'%self
.addr
.ip_str()
75 "A set of IP addresses specified as a list of networks"
77 self
.set=ipaddr
.ip_set()
80 self
.set.append(ipaddr
.network(x
[0],x
[1],
81 ipaddr
.DEMAND_NETWORK
))
83 return string
.join(map(lambda x
:'"%s/%s"'%(x
.ip_str(),
84 x
.mask
.netmask_bits_str
),
85 self
.set.as_list_of_networks()),",")
88 "A Diffie-Hellman group"
93 return 'diffie-hellman("%s","%s")'%(self
.mod
,self
.gen
)
96 "A choice of hash function"
99 if (self
.ht
!='md5' and self
.ht
!='sha1'):
100 complain("unknown hash type %s"%(self
.ht
))
102 return '%s'%(self
.ht
)
106 def __init__(self
,w
):
109 return '<%s>'%(self
.addr
)
113 def __init__(self
,w
):
114 self
.n
=string
.atol(w
[1])
119 "A DNS name and UDP port number"
120 def __init__(self
,w
):
122 self
.port
=string
.atoi(w
[2])
123 if (self
.port
<1 or self
.port
>65535):
124 complain("invalid port number")
126 return '"%s"; port %d'%(self
.adr
,self
.port
)
130 def __init__(self
,w
):
131 self
.l
=string
.atoi(w
[1])
135 return 'rsa-public("%s","%s")'%(self
.e
,self
.n
)
137 # Possible properties of configuration nodes
139 'contact':(email
,"Contact address"),
140 'dh':(dhgroup
,"Diffie-Hellman group"),
141 'hash':(hash,"Hash function"),
142 'key-lifetime':(num
,"Maximum key lifetime (ms)"),
143 'setup-timeout':(num
,"Key setup timeout (ms)"),
144 'setup-retries':(num
,"Maximum key setup packet retries"),
145 'wait-time':(num
,"Time to wait after unsuccessful key setup (ms)"),
146 'renegotiate-time':(num
,"Time after key setup to begin renegotiation (ms)"),
147 'restrict-nets':(networks
,"Allowable networks"),
148 'networks':(networks
,"Claimed networks"),
149 'pubkey':(rsakey
,"RSA public site key"),
150 'peer':(single_ipaddr
,"Tunnel peer IP address"),
151 'address':(address
,"External contact address and port")
155 "Simply output a property - the default case"
156 return "%s %s;\n"%(name
,value
)
158 # All levels support these properties
160 'contact':(lambda name
,value
:"# Contact email address: %s\n"%(value)
),
167 'renegotiate-time':sp
,
168 'restrict-nets':(lambda name
,value
:"# restrict-nets %s\n"%value
)
172 "A level in the configuration hierarchy"
176 require_properties
={}
177 def __init__(self
,w
):
181 def indent(self
,w
,t
):
183 def prop_out(self
,n
):
184 return self
.allow_properties
[n
](n
,str(self
.properties
[n
]))
185 def output_props(self
,w
,ind
):
186 for i
in self
.properties
.keys():
187 if self
.allow_properties
[i
]:
189 w
.write("%s"%self
.prop_out(i
))
190 def output_data(self
,w
,ind
,np
):
192 w
.write("%s {\n"%(self
.name
))
193 self
.output_props(w
,ind
+2)
194 if self
.depth
==1: w
.write("\n");
195 for c
in self
.children
.values():
196 c
.output_data(w
,ind
+2,np
+self
.name
+"/")
200 class vpnlevel(level
):
201 "VPN level in the configuration hierarchy"
205 allow_properties
=global_properties
.copy()
207 'contact':"VPN admin contact address"
209 def __init__(self
,w
):
210 level
.__init__(self
,w
)
211 def output_vpnflat(self
,w
,ind
,h
):
212 "Output flattened list of site names for this VPN"
214 w
.write("%s {\n"%(self
.name
))
215 for i
in self
.children
.keys():
216 self
.children
[i
].output_vpnflat(w
,ind
+2,
217 h
+"/"+self
.name
+"/"+i
)
220 w
.write("all-sites %s;\n"%
221 string
.join(self
.children
.keys(),','))
225 class locationlevel(level
):
226 "Location level in the configuration hierarchy"
230 allow_properties
=global_properties
.copy()
232 'contact':"Location admin contact address",
234 def __init__(self
,w
):
235 level
.__init__(self
,w
)
237 def output_vpnflat(self
,w
,ind
,h
):
239 # The "h=h,self=self" abomination below exists because
240 # Python didn't support nested_scopes until version 2.1
241 w
.write("%s %s;\n"%(self
.name
,string
.join(
242 map(lambda x
,h
=h
,self
=self
:
243 h
+"/"+x
,self
.children
.keys()),',')))
245 class sitelevel(level
):
246 "Site level (i.e. a leafnode) in the configuration hierarchy"
250 allow_properties
=global_properties
.copy()
251 allow_properties
.update({
255 'pubkey':(lambda n
,v
:"key %s;\n"%v
)
258 'dh':"Diffie-Hellman group",
259 'contact':"Site admin contact address",
260 'address':"Site external access address",
261 'networks':"Networks claimed by the site",
262 'hash':"hash function",
263 'peer':"Gateway address of the site",
264 'pubkey':"RSA public key of the site"
266 def __init__(self
,w
):
267 level
.__init__(self
,w
)
268 def output_data(self
,w
,ind
,np
):
270 w
.write("%s {\n"%(self
.name
))
272 w
.write("name \"%s\";\n"%(np
+self
.name
))
273 self
.output_props(w
,ind
+2)
275 w
.write("link netlink {\n");
277 w
.write("routes %s;\n"%str
(self
.properties
["networks"]))
279 w
.write("ptp-address %s;\n"%str
(self
.properties
["peer"]))
285 # Levels in the configuration file
287 levels
={'vpn':vpnlevel
, 'location':locationlevel
, 'site':sitelevel
}
289 # Reserved vpn/location/site names
290 reserved
={'all-sites':None}
291 reserved
.update(keywords
)
292 reserved
.update(levels
)
295 "Complain about a particular input line"
297 print ("%s line %d: "%(file,line
))+msg
298 complaints
=complaints
+1
300 "Complain about something in general"
303 complaints
=complaints
+1
305 root
=level(['root','root']) # All vpns are children of this node
307 allow_defs
=0 # Level above which new definitions are permitted
309 def set_property(obj
,w
):
310 "Set a property on a configuration node"
311 if obj
.properties
.has_key(w
[0]):
312 complain("%s %s already has property %s defined"%
313 (obj
.type,obj
.name
,w
[0]))
315 obj
.properties
[w
[0]]=keywords
[w
[0]][0](w
)
318 "Process a configuration file line"
319 global allow_defs
, obstack
, root
323 current
=obstack
[len(obstack
)-1]
324 if keyword
=='end-definitions':
325 allow_defs
=sitelevel
.depth
328 if levels
.has_key(keyword
):
329 # We may go up any number of levels, but only down by one
330 newdepth
=levels
[keyword
].depth
331 currentdepth
=len(obstack
) # actually +1...
332 if newdepth
<=currentdepth
:
333 obstack
=obstack
[:newdepth
]
334 if newdepth
>currentdepth
:
335 complain("May not go from level %d to level %d"%
336 (currentdepth
-1,newdepth
))
337 # See if it's a new one (and whether that's permitted)
339 current
=obstack
[len(obstack
)-1]
340 if current
.children
.has_key(w
[1]):
342 current
=current
.children
[w
[1]]
343 if service
and group
and current
.depth
==2:
344 if group
!=current
.group
:
345 complain("Incorrect group!")
348 # Ignore depth check for now
349 nl
=levels
[keyword
](w
)
350 if nl
.depth
<allow_defs
:
351 complain("New definitions not allowed at "
353 current
.children
[w
[1]]=nl
355 obstack
.append(current
)
357 if current
.allow_properties
.has_key(keyword
):
358 set_property(current
,w
)
361 complain("Property %s not allowed at %s level"%
362 (keyword
,current
.type))
365 complain("unknown keyword '%s'"%(keyword)
)
367 def pfile(name
,lines
):
374 if (i
[0]=='#'): continue
375 if (i
[len(i
)-1]=='\n'): i
=i
[:len(i
)-1] # strip trailing LF
379 "Output include file for secnet configuration"
380 w
.write("# secnet sites file autogenerated by make-secnet-sites "
381 +"version %s\n"%VERSION
)
382 w
.write("# %s\n"%time
.asctime(time
.localtime(time
.time())))
383 w
.write("# Command line: %s\n\n"%string
.join(sys
.argv
))
385 # Raw VPN data section of file
386 w
.write("vpn-data {\n")
387 for i
in root
.children
.values():
388 i
.output_data(w
,2,"")
391 # Per-VPN flattened lists
393 for i
in root
.children
.values():
394 i
.output_vpnflat(w
,2,"vpn-data")
397 # Flattened list of sites
398 w
.write("all-sites %s;\n"%string
.join(map(lambda x
:"vpn/%s/all-sites"%
399 x
,root
.children
.keys()),","))
401 # Are we being invoked from userv?
403 # If we are, which group does the caller want to modify?
411 pfile("stdin",sys
.stdin
.readlines())
414 if sys
.argv
[1]=='-u':
416 print "Wrong number of arguments"
420 groupfiledir
=sys
.argv
[3]
421 sitesfile
=sys
.argv
[4]
423 if not os
.environ
.has_key("USERV_USER"):
424 print "Environment variable USERV_USER not found"
426 user
=os
.environ
["USERV_USER"]
427 # Check that group is in USERV_GROUP
428 if not os
.environ
.has_key("USERV_GROUP"):
429 print "Environment variable USERV_GROUP not found"
431 ugs
=os
.environ
["USERV_GROUP"]
433 for i
in string
.split(ugs
):
436 print "caller not in group %s"%group
439 headerinput
=f
.readlines()
441 pfile(header
,headerinput
)
442 userinput
=sys
.stdin
.readlines()
443 pfile("user input",userinput
)
446 print "Too many arguments"
449 pfile(sys
.argv
[1],f
.readlines())
453 of
=open(sys
.argv
[2],'w')
455 # Sanity check section
456 # Delete nodes where leaf=0 that have no children
459 "Number of leafnodes below node n"
461 for i
in n
.children
.keys():
462 if live(n
.children
[i
]): return 1
465 "Delete nodes that have no leafnode children"
466 for i
in n
.children
.keys():
467 delempty(n
.children
[i
])
468 if not live(n
.children
[i
]):
472 # Check that all constraints are met (as far as I can tell
473 # restrict-nets/networks/peer are the only special cases)
475 def checkconstraints(n
,p
,ra
):
477 new_p
.update(n
.properties
)
478 for i
in n
.require_properties
.keys():
479 if not new_p
.has_key(i
):
480 moan("%s %s is missing property %s"%
482 for i
in new_p
.keys():
483 if not n
.allow_properties
.has_key(i
):
484 moan("%s %s has forbidden property %s"%
486 # Check address range restrictions
487 if n
.properties
.has_key("restrict-nets"):
488 new_ra
=ra
.intersection(n
.properties
["restrict-nets"].set)
491 if n
.properties
.has_key("networks"):
492 # I'd like to do this:
493 # n.properties["networks"].set.is_subset(new_ra)
494 # but there isn't an is_subset() method
495 # Instead we see if we intersect with the complement of new_ra
496 rac
=new_ra
.complement()
497 i
=rac
.intersection(n
.properties
["networks"].set)
499 moan("%s %s networks out of bounds"%(n
.type,n
.name
))
500 if n
.properties
.has_key("peer"):
501 if not n
.properties
["networks"].set.contains(
502 n
.properties
["peer"].addr
):
503 moan("%s %s peer not in networks"%(n
.type,n
.name
))
504 for i
in n
.children
.keys():
505 checkconstraints(n
.children
[i
],new_p
,new_ra
)
507 checkconstraints(root
,{},ipaddr
.complete_set
)
510 if complaints
==1: print "There was 1 problem."
511 else: print "There were %d problems."%(complaints)
515 # Put the user's input into their group file, and rebuild the main
517 f
=open(groupfiledir
+"/T"+group
,'w')
518 f
.write("# Section submitted by user %s, %s\n"%
519 (user
,time
.asctime(time
.localtime(time
.time()))))
520 f
.write("# Checked by make-secnet-sites version %s\n\n"%VERSION
)
521 for i
in userinput
: f
.write(i
)
524 os
.rename(groupfiledir
+"/T"+group
,groupfiledir
+"/R"+group
)
525 f
=open(sitesfile
+"-tmp",'w')
526 f
.write("# sites file autogenerated by make-secnet-sites\n")
527 f
.write("# generated %s, invoked by %s\n"%
528 (time
.asctime(time
.localtime(time
.time())),user
))
529 f
.write("# use make-secnet-sites to turn this file into a\n")
530 f
.write("# valid /etc/secnet/sites.conf file\n\n")
531 for i
in headerinput
: f
.write(i
)
532 files
=os
.listdir(groupfiledir
)
535 j
=open(groupfiledir
+"/"+i
)
538 f
.write("# end of sites file\n")
540 os
.rename(sitesfile
+"-tmp",sitesfile
)