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 trad_dhgroup (basetype
):
107 "A Diffie-Hellman group"
108 def __init__(self
,w
):
112 return 'diffie-hellman("%s","%s")'%(self
.mod
,self
.gen
)
114 if w
[1] in ('x25519', 'x448'): return w
[1]
115 else: return trad_dhgroup(w
)
117 class hash (basetype
):
118 "A choice of hash function"
119 def __init__(self
,w
):
121 if (self
.ht
not in ('md5', 'sha1', 'sha512')):
122 complain("unknown hash type %s"%(self
.ht
))
124 return '%s'%(self
.ht
)
126 class email (basetype
):
128 def __init__(self
,w
):
131 return '<%s>'%(self
.addr
)
133 class boolean (basetype
):
135 def __init__(self
,w
):
136 if re
.match('[TtYy1]',w
[1]):
138 elif re
.match('[FfNn0]',w
[1]):
141 complain("invalid boolean value");
143 return ['False','True'][self
.b
]
145 class num (basetype
):
147 def __init__(self
,w
):
148 self
.n
=string
.atol(w
[1])
152 class address (basetype
):
153 "A DNS name and UDP port number"
154 def __init__(self
,w
):
156 self
.port
=string
.atoi(w
[2])
157 if (self
.port
<1 or self
.port
>65535):
158 complain("invalid port number")
160 return '"%s"; port %d'%(self
.adr
,self
.port
)
162 class rsakey (basetype
):
164 def __init__(self
,w
):
165 self
.l
=string
.atoi(w
[1])
169 return 'rsa-public("%s","%s")'%(self
.e
,self
.n
)
171 # Possible properties of configuration nodes
173 'contact':(email
,"Contact address"),
174 'dh':(listof(dhgroup
),"Diffie-Hellman group"),
175 'hash':(hash,"Hash function"),
176 'key-lifetime':(num
,"Maximum key lifetime (ms)"),
177 'setup-timeout':(num
,"Key setup timeout (ms)"),
178 'setup-retries':(num
,"Maximum key setup packet retries"),
179 'wait-time':(num
,"Time to wait after unsuccessful key setup (ms)"),
180 'renegotiate-time':(num
,"Time after key setup to begin renegotiation (ms)"),
181 'restrict-nets':(networks
,"Allowable networks"),
182 'networks':(networks
,"Claimed networks"),
183 'pubkey':(rsakey
,"RSA public site key"),
184 'peer':(single_ipaddr
,"Tunnel peer IP address"),
185 'address':(address
,"External contact address and port"),
186 'mobile':(boolean
,"Site is mobile"),
190 "Simply output a property - the default case"
191 return "%s %s;\n"%(name
,value
)
193 # All levels support these properties
195 'contact':(lambda name
,value
:"# Contact email address: %s\n"%(value)
),
202 'renegotiate-time':sp
,
203 'restrict-nets':(lambda name
,value
:"# restrict-nets %s\n"%value
),
207 "A level in the configuration hierarchy"
211 require_properties
={}
212 def __init__(self
,w
):
216 def indent(self
,w
,t
):
218 def prop_out(self
,n
):
219 return self
.allow_properties
[n
](n
,str(self
.properties
[n
]))
220 def output_props(self
,w
,ind
):
221 for i
in self
.properties
.keys():
222 if self
.allow_properties
[i
]:
224 w
.write("%s"%self
.prop_out(i
))
225 def output_data(self
,w
,ind
,np
):
227 w
.write("%s {\n"%(self
.name
))
228 self
.output_props(w
,ind
+2)
229 if self
.depth
==1: w
.write("\n");
230 for c
in self
.children
.values():
231 c
.output_data(w
,ind
+2,np
+self
.name
+"/")
235 class vpnlevel(level
):
236 "VPN level in the configuration hierarchy"
240 allow_properties
=global_properties
.copy()
242 'contact':"VPN admin contact address"
244 def __init__(self
,w
):
245 level
.__init__(self
,w
)
246 def output_vpnflat(self
,w
,ind
,h
):
247 "Output flattened list of site names for this VPN"
249 w
.write("%s {\n"%(self
.name
))
250 for i
in self
.children
.keys():
251 self
.children
[i
].output_vpnflat(w
,ind
+2,
252 h
+"/"+self
.name
+"/"+i
)
255 w
.write("all-sites %s;\n"%
256 string
.join(self
.children
.keys(),','))
260 class locationlevel(level
):
261 "Location level in the configuration hierarchy"
265 allow_properties
=global_properties
.copy()
267 'contact':"Location admin contact address",
269 def __init__(self
,w
):
270 level
.__init__(self
,w
)
272 def output_vpnflat(self
,w
,ind
,h
):
274 # The "h=h,self=self" abomination below exists because
275 # Python didn't support nested_scopes until version 2.1
276 w
.write("%s %s;\n"%(self
.name
,string
.join(
277 map(lambda x
,h
=h
,self
=self
:
278 h
+"/"+x
,self
.children
.keys()),',')))
280 class sitelevel(level
):
281 "Site level (i.e. a leafnode) in the configuration hierarchy"
285 allow_properties
=global_properties
.copy()
286 allow_properties
.update({
290 'pubkey':(lambda n
,v
:"key %s;\n"%v
),
294 'dh':"Diffie-Hellman group",
295 'contact':"Site admin contact address",
296 'networks':"Networks claimed by the site",
297 'hash':"hash function",
298 'peer':"Gateway address of the site",
299 'pubkey':"RSA public key of the site",
301 def __init__(self
,w
):
302 level
.__init__(self
,w
)
303 def output_data(self
,w
,ind
,np
):
305 w
.write("%s {\n"%(self
.name
))
307 w
.write("name \"%s\";\n"%(np
+self
.name
))
308 self
.output_props(w
,ind
+2)
310 w
.write("link netlink {\n");
312 w
.write("routes %s;\n"%str
(self
.properties
["networks"]))
314 w
.write("ptp-address %s;\n"%str
(self
.properties
["peer"]))
320 # Levels in the configuration file
322 levels
={'vpn':vpnlevel
, 'location':locationlevel
, 'site':sitelevel
}
324 # Reserved vpn/location/site names
325 reserved
={'all-sites':None}
326 reserved
.update(keywords
)
327 reserved
.update(levels
)
330 "Complain about a particular input line"
332 print ("%s line %d: "%(file,line
))+msg
333 complaints
=complaints
+1
335 "Complain about something in general"
338 complaints
=complaints
+1
340 root
=level(['root','root']) # All vpns are children of this node
342 allow_defs
=0 # Level above which new definitions are permitted
345 def set_property(obj
,w
):
346 "Set a property on a configuration node"
347 if obj
.properties
.has_key(w
[0]):
348 obj
.properties
[w
[0]].add(obj
,w
)
350 obj
.properties
[w
[0]]=keywords
[w
[0]][0](w
)
352 def pline(i
,allow_include
=False):
353 "Process a configuration file line"
354 global allow_defs
, obstack
, root
355 w
=string
.split(i
.rstrip('\n'))
356 if len(w
)==0: return [i
]
358 current
=obstack
[len(obstack
)-1]
359 if keyword
=='end-definitions':
360 allow_defs
=sitelevel
.depth
363 if keyword
=='include':
364 if not allow_include
:
365 complain("include not permitted here")
368 complain("include requires one argument")
370 newfile
=os
.path
.join(os
.path
.dirname(file),w
[1])
371 return pfilepath(newfile
,allow_include
=allow_include
)
372 if levels
.has_key(keyword
):
373 # We may go up any number of levels, but only down by one
374 newdepth
=levels
[keyword
].depth
375 currentdepth
=len(obstack
) # actually +1...
376 if newdepth
<=currentdepth
:
377 obstack
=obstack
[:newdepth
]
378 if newdepth
>currentdepth
:
379 complain("May not go from level %d to level %d"%
380 (currentdepth
-1,newdepth
))
381 # See if it's a new one (and whether that's permitted)
383 current
=obstack
[len(obstack
)-1]
384 if current
.children
.has_key(w
[1]):
386 current
=current
.children
[w
[1]]
387 if service
and group
and current
.depth
==2:
388 if group
!=current
.group
:
389 complain("Incorrect group!")
392 # Ignore depth check for now
393 nl
=levels
[keyword
](w
)
394 if nl
.depth
<allow_defs
:
395 complain("New definitions not allowed at "
397 # we risk crashing if we continue
399 current
.children
[w
[1]]=nl
401 obstack
.append(current
)
403 if not current
.allow_properties
.has_key(keyword
):
404 complain("Property %s not allowed at %s level"%
405 (keyword
,current
.type))
407 elif current
.depth
== vpnlevel
.depth
< allow_defs
:
408 complain("Not allowed to set VPN properties here")
411 set_property(current
,w
)
414 complain("unknown keyword '%s'"%(keyword)
)
416 def pfilepath(pathname
,allow_include
=False):
418 outlines
=pfile(pathname
,f
.readlines(),allow_include
=allow_include
)
422 def pfile(name
,lines
,allow_include
=False):
430 if (i
[0]=='#'): continue
431 outlines
+= pline(i
,allow_include
=allow_include
)
435 "Output include file for secnet configuration"
436 w
.write("# secnet sites file autogenerated by make-secnet-sites "
437 +"version %s\n"%VERSION
)
438 w
.write("# %s\n"%time
.asctime(time
.localtime(time
.time())))
439 w
.write("# Command line: %s\n\n"%string
.join(sys
.argv
))
441 # Raw VPN data section of file
442 w
.write(prefix
+"vpn-data {\n")
443 for i
in root
.children
.values():
444 i
.output_data(w
,2,"")
447 # Per-VPN flattened lists
448 w
.write(prefix
+"vpn {\n")
449 for i
in root
.children
.values():
450 i
.output_vpnflat(w
,2,prefix
+"vpn-data")
453 # Flattened list of sites
454 w
.write(prefix
+"all-sites %s;\n"%string
.join(
455 map(lambda x
:"%svpn/%s/all-sites"%(prefix
,x
),
456 root
.children
.keys()),","))
458 # Are we being invoked from userv?
460 # If we are, which group does the caller want to modify?
468 pfile("stdin",sys
.stdin
.readlines())
471 if sys
.argv
[1]=='-u':
473 print "Wrong number of arguments"
477 groupfiledir
=sys
.argv
[3]
478 sitesfile
=sys
.argv
[4]
480 if not os
.environ
.has_key("USERV_USER"):
481 print "Environment variable USERV_USER not found"
483 user
=os
.environ
["USERV_USER"]
484 # Check that group is in USERV_GROUP
485 if not os
.environ
.has_key("USERV_GROUP"):
486 print "Environment variable USERV_GROUP not found"
488 ugs
=os
.environ
["USERV_GROUP"]
490 for i
in string
.split(ugs
):
493 print "caller not in group %s"%group
495 headerinput
=pfilepath(header
,allow_include
=True)
496 userinput
=sys
.stdin
.readlines()
497 pfile("user input",userinput
)
499 if sys
.argv
[1]=='-P':
503 print "Too many arguments"
505 pfilepath(sys
.argv
[1])
508 of
=open(sys
.argv
[2],'w')
510 # Sanity check section
511 # Delete nodes where leaf=0 that have no children
514 "Number of leafnodes below node n"
516 for i
in n
.children
.keys():
517 if live(n
.children
[i
]): return 1
520 "Delete nodes that have no leafnode children"
521 for i
in n
.children
.keys():
522 delempty(n
.children
[i
])
523 if not live(n
.children
[i
]):
527 # Check that all constraints are met (as far as I can tell
528 # restrict-nets/networks/peer are the only special cases)
530 def checkconstraints(n
,p
,ra
):
532 new_p
.update(n
.properties
)
533 for i
in n
.require_properties
.keys():
534 if not new_p
.has_key(i
):
535 moan("%s %s is missing property %s"%
537 for i
in new_p
.keys():
538 if not n
.allow_properties
.has_key(i
):
539 moan("%s %s has forbidden property %s"%
541 # Check address range restrictions
542 if n
.properties
.has_key("restrict-nets"):
543 new_ra
=ra
.intersection(n
.properties
["restrict-nets"].set)
546 if n
.properties
.has_key("networks"):
547 if not n
.properties
["networks"].set <= new_ra
:
548 moan("%s %s networks out of bounds"%(n
.type,n
.name
))
549 if n
.properties
.has_key("peer"):
550 if not n
.properties
["networks"].set.contains(
551 n
.properties
["peer"].addr
):
552 moan("%s %s peer not in networks"%(n
.type,n
.name
))
553 for i
in n
.children
.keys():
554 checkconstraints(n
.children
[i
],new_p
,new_ra
)
556 checkconstraints(root
,{},ipaddrset
.complete_set())
559 if complaints
==1: print "There was 1 problem."
560 else: print "There were %d problems."%(complaints)
564 # Put the user's input into their group file, and rebuild the main
566 f
=open(groupfiledir
+"/T"+group
,'w')
567 f
.write("# Section submitted by user %s, %s\n"%
568 (user
,time
.asctime(time
.localtime(time
.time()))))
569 f
.write("# Checked by make-secnet-sites version %s\n\n"%VERSION
)
570 for i
in userinput
: f
.write(i
)
573 os
.rename(groupfiledir
+"/T"+group
,groupfiledir
+"/R"+group
)
574 f
=open(sitesfile
+"-tmp",'w')
575 f
.write("# sites file autogenerated by make-secnet-sites\n")
576 f
.write("# generated %s, invoked by %s\n"%
577 (time
.asctime(time
.localtime(time
.time())),user
))
578 f
.write("# use make-secnet-sites to turn this file into a\n")
579 f
.write("# valid /etc/secnet/sites.conf file\n\n")
580 for i
in headerinput
: f
.write(i
)
581 files
=os
.listdir(groupfiledir
)
584 j
=open(groupfiledir
+"/"+i
)
587 f
.write("# end of sites file\n")
589 os
.rename(sitesfile
+"-tmp",sitesfile
)