ec-field-test.c: Make the field-element type use internal format.
[secnet] / make-secnet-sites
1 #! /usr/bin/env python
2 #
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
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
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.
15 #
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.
19
20 """VPN sites file manipulation.
21
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.
25
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
28
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
31
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:
35
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37 group
38
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
41 fragment is:
42
43 reset
44 no-disconnect-hup
45 no-suppress-args
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
48
49 This program is part of secnet. It relies on the "ipaddr" library from
50 Cendio Systems AB.
51
52 """
53
54 import string
55 import time
56 import sys
57 import os
58 import getopt
59 import re
60
61 import ipaddr
62
63 sys.path.insert(0,"/usr/local/share/secnet")
64 sys.path.insert(0,"/usr/share/secnet")
65 import ipaddrset
66
67 VERSION="0.1.18"
68
69 # Classes describing possible datatypes in the configuration file
70
71 class basetype:
72 "Common protocol for configuration types."
73 def add(self,obj,w):
74 complain("%s %s already has property %s defined"%
75 (obj.type,obj.name,w[0]))
76
77 class 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))
86 def listof(subtype):
87 return lambda w: conflist(subtype, w)
88
89 class single_ipaddr (basetype):
90 "An IP address"
91 def __init__(self,w):
92 self.addr=ipaddr.IPAddress(w[1])
93 def __str__(self):
94 return '"%s"'%self.addr
95
96 class networks (basetype):
97 "A set of IP addresses specified as a list of networks"
98 def __init__(self,w):
99 self.set=ipaddrset.IPAddressSet()
100 for i in w[1:]:
101 x=ipaddr.IPNetwork(i,strict=True)
102 self.set.append([x])
103 def __str__(self):
104 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
105
106 class trad_dhgroup (basetype):
107 "A Diffie-Hellman group"
108 def __init__(self,w):
109 self.mod=w[1]
110 self.gen=w[2]
111 def __str__(self):
112 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
113 def dhgroup(w):
114 if w[1] in ('x25519', 'x448'): return w[1]
115 else: return trad_dhgroup(w)
116
117 class hash (basetype):
118 "A choice of hash function"
119 def __init__(self,w):
120 self.ht=w[1]
121 if (self.ht not in ('md5', 'sha1', 'sha512')):
122 complain("unknown hash type %s"%(self.ht))
123 def __str__(self):
124 return '%s'%(self.ht)
125
126 class email (basetype):
127 "An email address"
128 def __init__(self,w):
129 self.addr=w[1]
130 def __str__(self):
131 return '<%s>'%(self.addr)
132
133 class boolean (basetype):
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
145 class num (basetype):
146 "A decimal number"
147 def __init__(self,w):
148 self.n=string.atol(w[1])
149 def __str__(self):
150 return '%d'%(self.n)
151
152 class address (basetype):
153 "A DNS name and UDP port number"
154 def __init__(self,w):
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")
159 def __str__(self):
160 return '"%s"; port %d'%(self.adr,self.port)
161
162 class rsakey (basetype):
163 "An RSA public key"
164 def __init__(self,w):
165 self.l=string.atoi(w[1])
166 self.e=w[2]
167 self.n=w[3]
168 def __str__(self):
169 return 'rsa-public("%s","%s")'%(self.e,self.n)
170
171 # Possible properties of configuration nodes
172 keywords={
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"),
187 }
188
189 def 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
194 global_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,
203 'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
204 }
205
206 class 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
235 class 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
260 class 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
280 class 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,
290 'pubkey':(lambda n,v:"key %s;\n"%v),
291 'mobile':sp,
292 })
293 require_properties={
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",
300 }
301 def __init__(self,w):
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)
322 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
323
324 # Reserved vpn/location/site names
325 reserved={'all-sites':None}
326 reserved.update(keywords)
327 reserved.update(levels)
328
329 def complain(msg):
330 "Complain about a particular input line"
331 global complaints
332 print ("%s line %d: "%(file,line))+msg
333 complaints=complaints+1
334 def moan(msg):
335 "Complain about something in general"
336 global complaints
337 print msg;
338 complaints=complaints+1
339
340 root=level(['root','root']) # All vpns are children of this node
341 obstack=[root]
342 allow_defs=0 # Level above which new definitions are permitted
343 prefix=''
344
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)
349 else:
350 obj.properties[w[0]]=keywords[w[0]][0](w)
351
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]
357 keyword=w[0]
358 current=obstack[len(obstack)-1]
359 if keyword=='end-definitions':
360 allow_defs=sitelevel.depth
361 obstack=[root]
362 return [i]
363 if keyword=='include':
364 if not allow_include:
365 complain("include not permitted here")
366 return []
367 if len(w) != 2:
368 complain("include requires one argument")
369 return []
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)
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!")
390 else:
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)
397 # we risk crashing if we continue
398 sys.exit(1)
399 current.children[w[1]]=nl
400 current=nl
401 obstack.append(current)
402 return [i]
403 if not current.allow_properties.has_key(keyword):
404 complain("Property %s not allowed at %s level"%
405 (keyword,current.type))
406 return []
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]
413
414 complain("unknown keyword '%s'"%(keyword))
415
416 def pfilepath(pathname,allow_include=False):
417 f=open(pathname)
418 outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
419 f.close()
420 return outlines
421
422 def pfile(name,lines,allow_include=False):
423 "Process a file"
424 global file,line
425 file=name
426 line=0
427 outlines=[]
428 for i in lines:
429 line=line+1
430 if (i[0]=='#'): continue
431 outlines += pline(i,allow_include=allow_include)
432 return outlines
433
434 def outputsites(w):
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))
440
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,"")
445 w.write("};\n")
446
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")
451 w.write("};\n")
452
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()),","))
457
458 # Are we being invoked from userv?
459 service=0
460 # If we are, which group does the caller want to modify?
461 group=None
462
463 line=0
464 file=None
465 complaints=0
466
467 if len(sys.argv)<2:
468 pfile("stdin",sys.stdin.readlines())
469 of=sys.stdout
470 else:
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)
495 headerinput=pfilepath(header,allow_include=True)
496 userinput=sys.stdin.readlines()
497 pfile("user input",userinput)
498 else:
499 if sys.argv[1]=='-P':
500 prefix=sys.argv[2]
501 sys.argv[1:3]=[]
502 if len(sys.argv)>3:
503 print "Too many arguments"
504 sys.exit(1)
505 pfilepath(sys.argv[1])
506 of=sys.stdout
507 if len(sys.argv)>2:
508 of=open(sys.argv[2],'w')
509
510 # Sanity check section
511 # Delete nodes where leaf=0 that have no children
512
513 def 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
519 def 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]
525 delempty(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
530 def 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)
544 else:
545 new_ra=ra
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)
555
556 checkconstraints(root,{},ipaddrset.complete_set())
557
558 if complaints>0:
559 if complaints==1: print "There was 1 problem."
560 else: print "There were %d problems."%(complaints)
561 sys.exit(1)
562
563 if service:
564 # Put the user's input into their group file, and rebuild the main
565 # sites file
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)
571 f.write("\n")
572 f.close()
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)
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)
590 else:
591 outputsites(of)