ec-field-test.c: Make the field-element type use internal format.
[secnet] / make-secnet-sites
CommitLineData
3454dce4 1#! /usr/bin/env python
3454dce4 2#
c215a4bc
IJ
3# This file is part of secnet.
4# See README for full list of copyright holders.
5#
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
9c6a8729 8# the Free Software Foundation; either version 3 of the License, or
3454dce4
SE
9# (at your option) any later version.
10#
c215a4bc
IJ
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.
3454dce4
SE
15#
16# You should have received a copy of the GNU General Public License
c215a4bc
IJ
17# version 3 along with secnet; if not, see
18# https://www.gnu.org/licenses/gpl.html.
3454dce4
SE
19
20"""VPN sites file manipulation.
21
22This program enables VPN site descriptions to be submitted for
23inclusion in a central database, and allows the resulting database to
24be turned into a secnet configuration file.
25
26A database file can be turned into a secnet configuration file simply:
27make-secnet-sites.py [infile [outfile]]
28
29It would be wise to run secnet with the "--just-check-config" option
30before installing the output on a live system.
31
32The program expects to be invoked via userv to manage the database; it
33relies on the USERV_USER and USERV_GROUP environment variables. The
34command line arguments for this invocation are:
35
36make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37 group
38
39All but the last argument are expected to be set by userv; the 'group'
40argument is provided by the user. A suitable userv configuration file
41fragment is:
42
43reset
44no-disconnect-hup
45no-suppress-args
46cd ~/secnet/sites-test/
08f344d3 47execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
3454dce4
SE
48
49This program is part of secnet. It relies on the "ipaddr" library from
50Cendio Systems AB.
51
52"""
53
54import string
55import time
56import sys
57import os
3b83c932 58import getopt
040040f3 59import re
8dea8d37 60
71d65e4c
IJ
61import ipaddr
62
90ad8cd4
IJ
63sys.path.insert(0,"/usr/local/share/secnet")
64sys.path.insert(0,"/usr/share/secnet")
71d65e4c 65import ipaddrset
3454dce4 66
00152558 67VERSION="0.1.18"
3b83c932
SE
68
69# Classes describing possible datatypes in the configuration file
70
d252f549
MW
71class basetype:
72 "Common protocol for configuration types."
35744ac3
MW
73 def add(self,obj,w):
74 complain("%s %s already has property %s defined"%
75 (obj.type,obj.name,w[0]))
76
77class conflist:
78 "A list of some kind of configuration type."
79 def __init__(self,subtype,w):
80 self.subtype=subtype
81 self.list=[subtype(w)]
82 def add(self,obj,w):
83 self.list.append(self.subtype(w))
84 def __str__(self):
85 return ', '.join(map(str, self.list))
86def listof(subtype):
87 return lambda w: conflist(subtype, w)
d252f549
MW
88
89class single_ipaddr (basetype):
3b83c932
SE
90 "An IP address"
91 def __init__(self,w):
71d65e4c 92 self.addr=ipaddr.IPAddress(w[1])
3b83c932 93 def __str__(self):
71d65e4c 94 return '"%s"'%self.addr
3b83c932 95
d252f549 96class networks (basetype):
3b83c932 97 "A set of IP addresses specified as a list of networks"
3454dce4 98 def __init__(self,w):
71d65e4c 99 self.set=ipaddrset.IPAddressSet()
3454dce4 100 for i in w[1:]:
71d65e4c
IJ
101 x=ipaddr.IPNetwork(i,strict=True)
102 self.set.append([x])
3b83c932 103 def __str__(self):
71d65e4c 104 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
3454dce4 105
a4902c66 106class trad_dhgroup (basetype):
3b83c932 107 "A Diffie-Hellman group"
3454dce4 108 def __init__(self,w):
b2a56f7c
SE
109 self.mod=w[1]
110 self.gen=w[2]
3b83c932
SE
111 def __str__(self):
112 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
a4902c66
MW
113def dhgroup(w):
114 if w[1] in ('x25519', 'x448'): return w[1]
115 else: return trad_dhgroup(w)
3454dce4 116
d252f549 117class hash (basetype):
3b83c932 118 "A choice of hash function"
3454dce4 119 def __init__(self,w):
b2a56f7c 120 self.ht=w[1]
757aecff 121 if (self.ht not in ('md5', 'sha1', 'sha512')):
b2a56f7c 122 complain("unknown hash type %s"%(self.ht))
3b83c932
SE
123 def __str__(self):
124 return '%s'%(self.ht)
3454dce4 125
d252f549 126class email (basetype):
3b83c932 127 "An email address"
3454dce4 128 def __init__(self,w):
b2a56f7c 129 self.addr=w[1]
3b83c932
SE
130 def __str__(self):
131 return '<%s>'%(self.addr)
3454dce4 132
d252f549 133class boolean (basetype):
040040f3
IJ
134 "A boolean"
135 def __init__(self,w):
136 if re.match('[TtYy1]',w[1]):
137 self.b=True
138 elif re.match('[FfNn0]',w[1]):
139 self.b=False
140 else:
141 complain("invalid boolean value");
142 def __str__(self):
143 return ['False','True'][self.b]
144
d252f549 145class num (basetype):
3b83c932 146 "A decimal number"
3454dce4 147 def __init__(self,w):
b2a56f7c 148 self.n=string.atol(w[1])
3b83c932
SE
149 def __str__(self):
150 return '%d'%(self.n)
3454dce4 151
d252f549 152class address (basetype):
3b83c932 153 "A DNS name and UDP port number"
3454dce4 154 def __init__(self,w):
b2a56f7c
SE
155 self.adr=w[1]
156 self.port=string.atoi(w[2])
157 if (self.port<1 or self.port>65535):
158 complain("invalid port number")
3b83c932
SE
159 def __str__(self):
160 return '"%s"; port %d'%(self.adr,self.port)
3454dce4 161
d252f549 162class rsakey (basetype):
3b83c932 163 "An RSA public key"
3454dce4 164 def __init__(self,w):
b2a56f7c
SE
165 self.l=string.atoi(w[1])
166 self.e=w[2]
167 self.n=w[3]
3b83c932
SE
168 def __str__(self):
169 return 'rsa-public("%s","%s")'%(self.e,self.n)
170
171# Possible properties of configuration nodes
172keywords={
173 'contact':(email,"Contact address"),
a4902c66 174 'dh':(listof(dhgroup),"Diffie-Hellman group"),
3b83c932
SE
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"),
a25b1149 185 'address':(address,"External contact address and port"),
040040f3 186 'mobile':(boolean,"Site is mobile"),
3b83c932
SE
187}
188
189def sp(name,value):
190 "Simply output a property - the default case"
191 return "%s %s;\n"%(name,value)
192
193# All levels support these properties
194global_properties={
195 'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
196 'dh':sp,
197 'hash':sp,
198 'key-lifetime':sp,
199 'setup-timeout':sp,
200 'setup-retries':sp,
201 'wait-time':sp,
202 'renegotiate-time':sp,
a25b1149 203 'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
3b83c932
SE
204}
205
206class level:
207 "A level in the configuration hierarchy"
208 depth=0
209 leaf=0
210 allow_properties={}
211 require_properties={}
212 def __init__(self,w):
213 self.name=w[1]
214 self.properties={}
215 self.children={}
216 def indent(self,w,t):
217 w.write(" "[: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]:
223 self.indent(w,ind)
224 w.write("%s"%self.prop_out(i))
225 def output_data(self,w,ind,np):
226 self.indent(w,ind)
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+"/")
232 self.indent(w,ind)
233 w.write("};\n")
234
235class vpnlevel(level):
236 "VPN level in the configuration hierarchy"
237 depth=1
238 leaf=0
239 type="vpn"
240 allow_properties=global_properties.copy()
241 require_properties={
242 'contact':"VPN admin contact address"
243 }
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"
248 self.indent(w,ind)
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)
253 w.write("\n")
254 self.indent(w,ind+2)
255 w.write("all-sites %s;\n"%
256 string.join(self.children.keys(),','))
257 self.indent(w,ind)
258 w.write("};\n")
259
260class locationlevel(level):
261 "Location level in the configuration hierarchy"
262 depth=2
263 leaf=0
264 type="location"
265 allow_properties=global_properties.copy()
266 require_properties={
267 'contact':"Location admin contact address",
268 }
269 def __init__(self,w):
270 level.__init__(self,w)
271 self.group=w[2]
272 def output_vpnflat(self,w,ind,h):
273 self.indent(w,ind)
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()),',')))
279
280class sitelevel(level):
281 "Site level (i.e. a leafnode) in the configuration hierarchy"
282 depth=3
283 leaf=1
284 type="site"
285 allow_properties=global_properties.copy()
286 allow_properties.update({
287 'address':sp,
288 'networks':None,
289 'peer':None,
a25b1149 290 'pubkey':(lambda n,v:"key %s;\n"%v),
040040f3 291 'mobile':sp,
3b83c932
SE
292 })
293 require_properties={
294 'dh':"Diffie-Hellman group",
295 'contact':"Site admin contact address",
3b83c932
SE
296 'networks':"Networks claimed by the site",
297 'hash':"hash function",
298 'peer':"Gateway address of the site",
a25b1149 299 'pubkey':"RSA public key of the site",
3b83c932 300 }
3454dce4 301 def __init__(self,w):
3b83c932
SE
302 level.__init__(self,w)
303 def output_data(self,w,ind,np):
304 self.indent(w,ind)
305 w.write("%s {\n"%(self.name))
306 self.indent(w,ind+2)
307 w.write("name \"%s\";\n"%(np+self.name))
308 self.output_props(w,ind+2)
309 self.indent(w,ind+2)
310 w.write("link netlink {\n");
311 self.indent(w,ind+4)
312 w.write("routes %s;\n"%str(self.properties["networks"]))
313 self.indent(w,ind+4)
314 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
315 self.indent(w,ind+2)
316 w.write("};\n")
317 self.indent(w,ind)
318 w.write("};\n")
319
320# Levels in the configuration file
321# (depth,properties)
322levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
323
324# Reserved vpn/location/site names
325reserved={'all-sites':None}
326reserved.update(keywords)
327reserved.update(levels)
3454dce4
SE
328
329def complain(msg):
3b83c932 330 "Complain about a particular input line"
3454dce4
SE
331 global complaints
332 print ("%s line %d: "%(file,line))+msg
333 complaints=complaints+1
334def moan(msg):
3b83c932 335 "Complain about something in general"
3454dce4
SE
336 global complaints
337 print msg;
338 complaints=complaints+1
339
3b83c932
SE
340root=level(['root','root']) # All vpns are children of this node
341obstack=[root]
342allow_defs=0 # Level above which new definitions are permitted
26f727b9 343prefix=''
3b83c932
SE
344
345def set_property(obj,w):
346 "Set a property on a configuration node"
347 if obj.properties.has_key(w[0]):
35744ac3 348 obj.properties[w[0]].add(obj,w)
3b83c932
SE
349 else:
350 obj.properties[w[0]]=keywords[w[0]][0](w)
3454dce4 351
c4497add 352def pline(i,allow_include=False):
3b83c932
SE
353 "Process a configuration file line"
354 global allow_defs, obstack, root
6d8cd9b2 355 w=string.split(i.rstrip('\n'))
433b0ae8 356 if len(w)==0: return [i]
3454dce4 357 keyword=w[0]
3b83c932 358 current=obstack[len(obstack)-1]
3454dce4 359 if keyword=='end-definitions':
3b83c932
SE
360 allow_defs=sitelevel.depth
361 obstack=[root]
433b0ae8 362 return [i]
c4497add
IJ
363 if keyword=='include':
364 if not allow_include:
365 complain("include not permitted here")
433b0ae8 366 return []
c4497add
IJ
367 if len(w) != 2:
368 complain("include requires one argument")
433b0ae8 369 return []
c4497add 370 newfile=os.path.join(os.path.dirname(file),w[1])
433b0ae8 371 return pfilepath(newfile,allow_include=allow_include)
3b83c932
SE
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)
382 # or an existing one
383 current=obstack[len(obstack)-1]
384 if current.children.has_key(w[1]):
385 # Not new
386 current=current.children[w[1]]
387 if service and group and current.depth==2:
388 if group!=current.group:
389 complain("Incorrect group!")
3454dce4 390 else:
3b83c932
SE
391 # New
392 # Ignore depth check for now
393 nl=levels[keyword](w)
394 if nl.depth<allow_defs:
395 complain("New definitions not allowed at "
396 "level %d"%nl.depth)
4a9b680b
IJ
397 # we risk crashing if we continue
398 sys.exit(1)
3b83c932
SE
399 current.children[w[1]]=nl
400 current=nl
401 obstack.append(current)
433b0ae8 402 return [i]
8644ac83 403 if not current.allow_properties.has_key(keyword):
3b83c932
SE
404 complain("Property %s not allowed at %s level"%
405 (keyword,current.type))
433b0ae8 406 return []
8644ac83
MW
407 elif current.depth == vpnlevel.depth < allow_defs:
408 complain("Not allowed to set VPN properties here")
409 return []
410 else:
411 set_property(current,w)
412 return [i]
3b83c932
SE
413
414 complain("unknown keyword '%s'"%(keyword))
3454dce4 415
c4497add 416def pfilepath(pathname,allow_include=False):
9b8369e0 417 f=open(pathname)
433b0ae8 418 outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
9b8369e0 419 f.close()
433b0ae8 420 return outlines
9b8369e0 421
c4497add 422def pfile(name,lines,allow_include=False):
3b83c932 423 "Process a file"
3454dce4
SE
424 global file,line
425 file=name
426 line=0
433b0ae8 427 outlines=[]
3454dce4
SE
428 for i in lines:
429 line=line+1
430 if (i[0]=='#'): continue
433b0ae8
IJ
431 outlines += pline(i,allow_include=allow_include)
432 return outlines
3454dce4
SE
433
434def outputsites(w):
3b83c932
SE
435 "Output include file for secnet configuration"
436 w.write("# secnet sites file autogenerated by make-secnet-sites "
3454dce4 437 +"version %s\n"%VERSION)
3b83c932
SE
438 w.write("# %s\n"%time.asctime(time.localtime(time.time())))
439 w.write("# Command line: %s\n\n"%string.join(sys.argv))
3454dce4
SE
440
441 # Raw VPN data section of file
26f727b9 442 w.write(prefix+"vpn-data {\n")
3b83c932
SE
443 for i in root.children.values():
444 i.output_data(w,2,"")
3454dce4
SE
445 w.write("};\n")
446
447 # Per-VPN flattened lists
26f727b9 448 w.write(prefix+"vpn {\n")
3b83c932 449 for i in root.children.values():
26f727b9 450 i.output_vpnflat(w,2,prefix+"vpn-data")
3454dce4
SE
451 w.write("};\n")
452
453 # Flattened list of sites
26f727b9
IJ
454 w.write(prefix+"all-sites %s;\n"%string.join(
455 map(lambda x:"%svpn/%s/all-sites"%(prefix,x),
456 root.children.keys()),","))
3454dce4
SE
457
458# Are we being invoked from userv?
459service=0
460# If we are, which group does the caller want to modify?
461group=None
462
3454dce4
SE
463line=0
464file=None
465complaints=0
466
3454dce4
SE
467if len(sys.argv)<2:
468 pfile("stdin",sys.stdin.readlines())
469 of=sys.stdout
470else:
471 if sys.argv[1]=='-u':
472 if len(sys.argv)!=6:
473 print "Wrong number of arguments"
474 sys.exit(1)
475 service=1
476 header=sys.argv[2]
477 groupfiledir=sys.argv[3]
478 sitesfile=sys.argv[4]
479 group=sys.argv[5]
480 if not os.environ.has_key("USERV_USER"):
481 print "Environment variable USERV_USER not found"
482 sys.exit(1)
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"
487 sys.exit(1)
488 ugs=os.environ["USERV_GROUP"]
489 ok=0
490 for i in string.split(ugs):
491 if group==i: ok=1
492 if not ok:
493 print "caller not in group %s"%group
494 sys.exit(1)
5b77d1a9 495 headerinput=pfilepath(header,allow_include=True)
3454dce4
SE
496 userinput=sys.stdin.readlines()
497 pfile("user input",userinput)
498 else:
26f727b9
IJ
499 if sys.argv[1]=='-P':
500 prefix=sys.argv[2]
501 sys.argv[1:3]=[]
3454dce4
SE
502 if len(sys.argv)>3:
503 print "Too many arguments"
504 sys.exit(1)
21fd3a92 505 pfilepath(sys.argv[1])
3454dce4
SE
506 of=sys.stdout
507 if len(sys.argv)>2:
508 of=open(sys.argv[2],'w')
509
510# Sanity check section
3b83c932
SE
511# Delete nodes where leaf=0 that have no children
512
513def live(n):
514 "Number of leafnodes below node n"
515 if n.leaf: return 1
516 for i in n.children.keys():
517 if live(n.children[i]): return 1
518 return 0
519def delempty(n):
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]):
524 del n.children[i]
525delempty(root)
526
527# Check that all constraints are met (as far as I can tell
528# restrict-nets/networks/peer are the only special cases)
529
530def checkconstraints(n,p,ra):
531 new_p=p.copy()
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"%
536 (n.type,n.name,i))
537 for i in new_p.keys():
538 if not n.allow_properties.has_key(i):
539 moan("%s %s has forbidden property %s"%
540 (n.type,n.name,i))
541 # Check address range restrictions
542 if n.properties.has_key("restrict-nets"):
543 new_ra=ra.intersection(n.properties["restrict-nets"].set)
3454dce4 544 else:
3b83c932
SE
545 new_ra=ra
546 if n.properties.has_key("networks"):
71d65e4c 547 if not n.properties["networks"].set <= new_ra:
3b83c932
SE
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)
555
71d65e4c 556checkconstraints(root,{},ipaddrset.complete_set())
3454dce4
SE
557
558if complaints>0:
559 if complaints==1: print "There was 1 problem."
560 else: print "There were %d problems."%(complaints)
561 sys.exit(1)
562
563if service:
564 # Put the user's input into their group file, and rebuild the main
565 # sites file
08f344d3 566 f=open(groupfiledir+"/T"+group,'w')
3454dce4
SE
567 f.write("# Section submitted by user %s, %s\n"%
568 (user,time.asctime(time.localtime(time.time()))))
3b83c932 569 f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
3454dce4
SE
570 for i in userinput: f.write(i)
571 f.write("\n")
572 f.close()
08f344d3
SE
573 os.rename(groupfiledir+"/T"+group,groupfiledir+"/R"+group)
574 f=open(sitesfile+"-tmp",'w')
ff05a229 575 f.write("# sites file autogenerated by make-secnet-sites\n")
08f344d3
SE
576 f.write("# generated %s, invoked by %s\n"%
577 (time.asctime(time.localtime(time.time())),user))
ff05a229 578 f.write("# use make-secnet-sites to turn this file into a\n")
08f344d3
SE
579 f.write("# valid /etc/secnet/sites.conf file\n\n")
580 for i in headerinput: f.write(i)
581 files=os.listdir(groupfiledir)
582 for i in files:
583 if i[0]=='R':
584 j=open(groupfiledir+"/"+i)
585 f.write(j.read())
586 j.close()
587 f.write("# end of sites file\n")
588 f.close()
589 os.rename(sitesfile+"-tmp",sitesfile)
3454dce4
SE
590else:
591 outputsites(of)