3 # This file is part of secnet.
4 # See README for full list of copyright holders.
6 # secnet is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # secnet is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # version 3 along with secnet; if not, see
18 # https://www.gnu.org/licenses/gpl.html.
20 """VPN sites file manipulation.
22 This program enables VPN site descriptions to be submitted for
23 inclusion in a central database, and allows the resulting database to
24 be turned into a secnet configuration file.
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
32 The program expects to be invoked via userv to manage the database; it
33 relies on the USERV_USER and USERV_GROUP environment variables. The
34 command line arguments for this invocation are:
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
39 All but the last argument are expected to be set by userv; the 'group'
40 argument is provided by the user. A suitable userv configuration file
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
49 This program is part of secnet. It relies on the "ipaddr" library from
63 sys
.path
.insert(0,"/usr/local/share/secnet")
64 sys
.path
.insert(0,"/usr/share/secnet")
69 # Classes describing possible datatypes in the configuration file
72 "Common protocol for configuration types."
74 complain("%s %s already has property %s defined"%
75 (obj
.type,obj
.name
,w
[0]))
78 "A list of some kind of configuration type."
79 def __init__(self
,subtype
,w
):
81 self
.list=[subtype(w
)]
83 self
.list.append(self
.subtype(w
))
85 return ', '.join(map(str, self
.list))
87 return lambda w
: conflist(subtype
, w
)
89 class single_ipaddr (basetype
):
92 self
.addr
=ipaddr
.IPAddress(w
[1])
94 return '"%s"'%self
.addr
96 class networks (basetype
):
97 "A set of IP addresses specified as a list of networks"
99 self
.set=ipaddrset
.IPAddressSet()
101 x
=ipaddr
.IPNetwork(i
,strict
=True)
104 return ",".join(map((lambda n
: '"%s"'%n
), self
.set.networks()))
106 class dhgroup (basetype
):
107 "A Diffie-Hellman group"
108 def __init__(self
,w
):
112 return 'diffie-hellman("%s","%s")'%(self
.mod
,self
.gen
)
114 class hash (basetype
):
115 "A choice of hash function"
116 def __init__(self
,w
):
118 if (self
.ht
not in ('md5', 'sha1', 'sha512')):
119 complain("unknown hash type %s"%(self
.ht
))
121 return '%s'%(self
.ht
)
123 class email (basetype
):
125 def __init__(self
,w
):
128 return '<%s>'%(self
.addr
)
130 class boolean (basetype
):
132 def __init__(self
,w
):
133 if re
.match('[TtYy1]',w
[1]):
135 elif re
.match('[FfNn0]',w
[1]):
138 complain("invalid boolean value");
140 return ['False','True'][self
.b
]
142 class num (basetype
):
144 def __init__(self
,w
):
145 self
.n
=string
.atol(w
[1])
149 class address (basetype
):
150 "A DNS name and UDP port number"
151 def __init__(self
,w
):
153 self
.port
=string
.atoi(w
[2])
154 if (self
.port
<1 or self
.port
>65535):
155 complain("invalid port number")
157 return '"%s"; port %d'%(self
.adr
,self
.port
)
159 class rsakey (basetype
):
161 def __init__(self
,w
):
162 self
.l
=string
.atoi(w
[1])
166 return 'rsa-public("%s","%s")'%(self
.e
,self
.n
)
168 # Possible properties of configuration nodes
170 'contact':(email
,"Contact address"),
171 'dh':(dhgroup
,"Diffie-Hellman group"),
172 'hash':(hash,"Hash function"),
173 'key-lifetime':(num
,"Maximum key lifetime (ms)"),
174 'setup-timeout':(num
,"Key setup timeout (ms)"),
175 'setup-retries':(num
,"Maximum key setup packet retries"),
176 'wait-time':(num
,"Time to wait after unsuccessful key setup (ms)"),
177 'renegotiate-time':(num
,"Time after key setup to begin renegotiation (ms)"),
178 'restrict-nets':(networks
,"Allowable networks"),
179 'networks':(networks
,"Claimed networks"),
180 'pubkey':(rsakey
,"RSA public site key"),
181 'peer':(single_ipaddr
,"Tunnel peer IP address"),
182 'address':(address
,"External contact address and port"),
183 'mobile':(boolean
,"Site is mobile"),
187 "Simply output a property - the default case"
188 return "%s %s;\n"%(name
,value
)
190 # All levels support these properties
192 'contact':(lambda name
,value
:"# Contact email address: %s\n"%(value)
),
199 'renegotiate-time':sp
,
200 'restrict-nets':(lambda name
,value
:"# restrict-nets %s\n"%value
),
204 "A level in the configuration hierarchy"
208 require_properties
={}
209 def __init__(self
,w
):
213 def indent(self
,w
,t
):
215 def prop_out(self
,n
):
216 return self
.allow_properties
[n
](n
,str(self
.properties
[n
]))
217 def output_props(self
,w
,ind
):
218 for i
in self
.properties
.keys():
219 if self
.allow_properties
[i
]:
221 w
.write("%s"%self
.prop_out(i
))
222 def output_data(self
,w
,ind
,np
):
224 w
.write("%s {\n"%(self
.name
))
225 self
.output_props(w
,ind
+2)
226 if self
.depth
==1: w
.write("\n");
227 for c
in self
.children
.values():
228 c
.output_data(w
,ind
+2,np
+self
.name
+"/")
232 class vpnlevel(level
):
233 "VPN level in the configuration hierarchy"
237 allow_properties
=global_properties
.copy()
239 'contact':"VPN admin contact address"
241 def __init__(self
,w
):
242 level
.__init__(self
,w
)
243 def output_vpnflat(self
,w
,ind
,h
):
244 "Output flattened list of site names for this VPN"
246 w
.write("%s {\n"%(self
.name
))
247 for i
in self
.children
.keys():
248 self
.children
[i
].output_vpnflat(w
,ind
+2,
249 h
+"/"+self
.name
+"/"+i
)
252 w
.write("all-sites %s;\n"%
253 string
.join(self
.children
.keys(),','))
257 class locationlevel(level
):
258 "Location level in the configuration hierarchy"
262 allow_properties
=global_properties
.copy()
264 'contact':"Location admin contact address",
266 def __init__(self
,w
):
267 level
.__init__(self
,w
)
269 def output_vpnflat(self
,w
,ind
,h
):
271 # The "h=h,self=self" abomination below exists because
272 # Python didn't support nested_scopes until version 2.1
273 w
.write("%s %s;\n"%(self
.name
,string
.join(
274 map(lambda x
,h
=h
,self
=self
:
275 h
+"/"+x
,self
.children
.keys()),',')))
277 class sitelevel(level
):
278 "Site level (i.e. a leafnode) in the configuration hierarchy"
282 allow_properties
=global_properties
.copy()
283 allow_properties
.update({
287 'pubkey':(lambda n
,v
:"key %s;\n"%v
),
291 'dh':"Diffie-Hellman group",
292 'contact':"Site admin contact address",
293 'networks':"Networks claimed by the site",
294 'hash':"hash function",
295 'peer':"Gateway address of the site",
296 'pubkey':"RSA public key of the site",
298 def __init__(self
,w
):
299 level
.__init__(self
,w
)
300 def output_data(self
,w
,ind
,np
):
302 w
.write("%s {\n"%(self
.name
))
304 w
.write("name \"%s\";\n"%(np
+self
.name
))
305 self
.output_props(w
,ind
+2)
307 w
.write("link netlink {\n");
309 w
.write("routes %s;\n"%str
(self
.properties
["networks"]))
311 w
.write("ptp-address %s;\n"%str
(self
.properties
["peer"]))
317 # Levels in the configuration file
319 levels
={'vpn':vpnlevel
, 'location':locationlevel
, 'site':sitelevel
}
321 # Reserved vpn/location/site names
322 reserved
={'all-sites':None}
323 reserved
.update(keywords
)
324 reserved
.update(levels
)
327 "Complain about a particular input line"
329 print ("%s line %d: "%(file,line
))+msg
330 complaints
=complaints
+1
332 "Complain about something in general"
335 complaints
=complaints
+1
337 root
=level(['root','root']) # All vpns are children of this node
339 allow_defs
=0 # Level above which new definitions are permitted
342 def set_property(obj
,w
):
343 "Set a property on a configuration node"
344 if obj
.properties
.has_key(w
[0]):
345 obj
.properties
[w
[0]].add(obj
,w
)
347 obj
.properties
[w
[0]]=keywords
[w
[0]][0](w
)
349 def pline(i
,allow_include
=False):
350 "Process a configuration file line"
351 global allow_defs
, obstack
, root
352 w
=string
.split(i
.rstrip('\n'))
353 if len(w
)==0: return [i
]
355 current
=obstack
[len(obstack
)-1]
356 if keyword
=='end-definitions':
357 allow_defs
=sitelevel
.depth
360 if keyword
=='include':
361 if not allow_include
:
362 complain("include not permitted here")
365 complain("include requires one argument")
367 newfile
=os
.path
.join(os
.path
.dirname(file),w
[1])
368 return pfilepath(newfile
,allow_include
=allow_include
)
369 if levels
.has_key(keyword
):
370 # We may go up any number of levels, but only down by one
371 newdepth
=levels
[keyword
].depth
372 currentdepth
=len(obstack
) # actually +1...
373 if newdepth
<=currentdepth
:
374 obstack
=obstack
[:newdepth
]
375 if newdepth
>currentdepth
:
376 complain("May not go from level %d to level %d"%
377 (currentdepth
-1,newdepth
))
378 # See if it's a new one (and whether that's permitted)
380 current
=obstack
[len(obstack
)-1]
381 if current
.children
.has_key(w
[1]):
383 current
=current
.children
[w
[1]]
384 if service
and group
and current
.depth
==2:
385 if group
!=current
.group
:
386 complain("Incorrect group!")
389 # Ignore depth check for now
390 nl
=levels
[keyword
](w
)
391 if nl
.depth
<allow_defs
:
392 complain("New definitions not allowed at "
394 # we risk crashing if we continue
396 current
.children
[w
[1]]=nl
398 obstack
.append(current
)
400 if not current
.allow_properties
.has_key(keyword
):
401 complain("Property %s not allowed at %s level"%
402 (keyword
,current
.type))
404 elif current
.depth
== vpnlevel
.depth
< allow_defs
:
405 complain("Not allowed to set VPN properties here")
408 set_property(current
,w
)
411 complain("unknown keyword '%s'"%(keyword)
)
413 def pfilepath(pathname
,allow_include
=False):
415 outlines
=pfile(pathname
,f
.readlines(),allow_include
=allow_include
)
419 def pfile(name
,lines
,allow_include
=False):
427 if (i
[0]=='#'): continue
428 outlines
+= pline(i
,allow_include
=allow_include
)
432 "Output include file for secnet configuration"
433 w
.write("# secnet sites file autogenerated by make-secnet-sites "
434 +"version %s\n"%VERSION
)
435 w
.write("# %s\n"%time
.asctime(time
.localtime(time
.time())))
436 w
.write("# Command line: %s\n\n"%string
.join(sys
.argv
))
438 # Raw VPN data section of file
439 w
.write(prefix
+"vpn-data {\n")
440 for i
in root
.children
.values():
441 i
.output_data(w
,2,"")
444 # Per-VPN flattened lists
445 w
.write(prefix
+"vpn {\n")
446 for i
in root
.children
.values():
447 i
.output_vpnflat(w
,2,prefix
+"vpn-data")
450 # Flattened list of sites
451 w
.write(prefix
+"all-sites %s;\n"%string
.join(
452 map(lambda x
:"%svpn/%s/all-sites"%(prefix
,x
),
453 root
.children
.keys()),","))
455 # Are we being invoked from userv?
457 # If we are, which group does the caller want to modify?
465 pfile("stdin",sys
.stdin
.readlines())
468 if sys
.argv
[1]=='-u':
470 print "Wrong number of arguments"
474 groupfiledir
=sys
.argv
[3]
475 sitesfile
=sys
.argv
[4]
477 if not os
.environ
.has_key("USERV_USER"):
478 print "Environment variable USERV_USER not found"
480 user
=os
.environ
["USERV_USER"]
481 # Check that group is in USERV_GROUP
482 if not os
.environ
.has_key("USERV_GROUP"):
483 print "Environment variable USERV_GROUP not found"
485 ugs
=os
.environ
["USERV_GROUP"]
487 for i
in string
.split(ugs
):
490 print "caller not in group %s"%group
492 headerinput
=pfilepath(header
,allow_include
=True)
493 userinput
=sys
.stdin
.readlines()
494 pfile("user input",userinput
)
496 if sys
.argv
[1]=='-P':
500 print "Too many arguments"
502 pfilepath(sys
.argv
[1])
505 of
=open(sys
.argv
[2],'w')
507 # Sanity check section
508 # Delete nodes where leaf=0 that have no children
511 "Number of leafnodes below node n"
513 for i
in n
.children
.keys():
514 if live(n
.children
[i
]): return 1
517 "Delete nodes that have no leafnode children"
518 for i
in n
.children
.keys():
519 delempty(n
.children
[i
])
520 if not live(n
.children
[i
]):
524 # Check that all constraints are met (as far as I can tell
525 # restrict-nets/networks/peer are the only special cases)
527 def checkconstraints(n
,p
,ra
):
529 new_p
.update(n
.properties
)
530 for i
in n
.require_properties
.keys():
531 if not new_p
.has_key(i
):
532 moan("%s %s is missing property %s"%
534 for i
in new_p
.keys():
535 if not n
.allow_properties
.has_key(i
):
536 moan("%s %s has forbidden property %s"%
538 # Check address range restrictions
539 if n
.properties
.has_key("restrict-nets"):
540 new_ra
=ra
.intersection(n
.properties
["restrict-nets"].set)
543 if n
.properties
.has_key("networks"):
544 if not n
.properties
["networks"].set <= new_ra
:
545 moan("%s %s networks out of bounds"%(n
.type,n
.name
))
546 if n
.properties
.has_key("peer"):
547 if not n
.properties
["networks"].set.contains(
548 n
.properties
["peer"].addr
):
549 moan("%s %s peer not in networks"%(n
.type,n
.name
))
550 for i
in n
.children
.keys():
551 checkconstraints(n
.children
[i
],new_p
,new_ra
)
553 checkconstraints(root
,{},ipaddrset
.complete_set())
556 if complaints
==1: print "There was 1 problem."
557 else: print "There were %d problems."%(complaints)
561 # Put the user's input into their group file, and rebuild the main
563 f
=open(groupfiledir
+"/T"+group
,'w')
564 f
.write("# Section submitted by user %s, %s\n"%
565 (user
,time
.asctime(time
.localtime(time
.time()))))
566 f
.write("# Checked by make-secnet-sites version %s\n\n"%VERSION
)
567 for i
in userinput
: f
.write(i
)
570 os
.rename(groupfiledir
+"/T"+group
,groupfiledir
+"/R"+group
)
571 f
=open(sitesfile
+"-tmp",'w')
572 f
.write("# sites file autogenerated by make-secnet-sites\n")
573 f
.write("# generated %s, invoked by %s\n"%
574 (time
.asctime(time
.localtime(time
.time())),user
))
575 f
.write("# use make-secnet-sites to turn this file into a\n")
576 f
.write("# valid /etc/secnet/sites.conf file\n\n")
577 for i
in headerinput
: f
.write(i
)
578 files
=os
.listdir(groupfiledir
)
581 j
=open(groupfiledir
+"/"+i
)
584 f
.write("# end of sites file\n")
586 os
.rename(sitesfile
+"-tmp",sitesfile
)