#! /usr/bin/env python # Copyright (C) 2001-2002 Stephen Early # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """VPN sites file manipulation. This program enables VPN site descriptions to be submitted for inclusion in a central database, and allows the resulting database to be turned into a secnet configuration file. A database file can be turned into a secnet configuration file simply: make-secnet-sites.py [infile [outfile]] It would be wise to run secnet with the "--just-check-config" option before installing the output on a live system. The program expects to be invoked via userv to manage the database; it relies on the USERV_USER and USERV_GROUP environment variables. The command line arguments for this invocation are: make-secnet-sites.py -u header-filename groupfiles-directory output-file \ group All but the last argument are expected to be set by userv; the 'group' argument is provided by the user. A suitable userv configuration file fragment is: reset no-disconnect-hup no-suppress-args cd ~/secnet/sites-test/ execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites This program is part of secnet. It relies on the "ipaddr" library from Cendio Systems AB. """ import string import time import sys import os import getopt import re # The ipaddr library is installed as part of secnet sys.path.insert(0,"/usr/local/share/secnet") sys.path.insert(0,"/usr/share/secnet") import ipaddr VERSION="0.1.18" # Classes describing possible datatypes in the configuration file class single_ipaddr: "An IP address" def __init__(self,w): self.addr=ipaddr.ipaddr(w[1]) def __str__(self): return '"%s"'%self.addr.ip_str() class networks: "A set of IP addresses specified as a list of networks" def __init__(self,w): self.set=ipaddr.ip_set() for i in w[1:]: x=string.split(i,"/") self.set.append(ipaddr.network(x[0],x[1], ipaddr.DEMAND_NETWORK)) def __str__(self): return string.join(map(lambda x:'"%s/%s"'%(x.ip_str(), x.mask.netmask_bits_str), self.set.as_list_of_networks()),",") class dhgroup: "A Diffie-Hellman group" def __init__(self,w): self.mod=w[1] self.gen=w[2] def __str__(self): return 'diffie-hellman("%s","%s")'%(self.mod,self.gen) class hash: "A choice of hash function" def __init__(self,w): self.ht=w[1] if (self.ht!='md5' and self.ht!='sha1'): complain("unknown hash type %s"%(self.ht)) def __str__(self): return '%s'%(self.ht) class email: "An email address" def __init__(self,w): self.addr=w[1] def __str__(self): return '<%s>'%(self.addr) class boolean: "A boolean" def __init__(self,w): if re.match('[TtYy1]',w[1]): self.b=True elif re.match('[FfNn0]',w[1]): self.b=False else: complain("invalid boolean value"); def __str__(self): return ['False','True'][self.b] class num: "A decimal number" def __init__(self,w): self.n=string.atol(w[1]) def __str__(self): return '%d'%(self.n) class address: "A DNS name and UDP port number" def __init__(self,w): self.adr=w[1] self.port=string.atoi(w[2]) if (self.port<1 or self.port>65535): complain("invalid port number") def __str__(self): return '"%s"; port %d'%(self.adr,self.port) class rsakey: "An RSA public key" def __init__(self,w): self.l=string.atoi(w[1]) self.e=w[2] self.n=w[3] def __str__(self): return 'rsa-public("%s","%s")'%(self.e,self.n) # Possible properties of configuration nodes keywords={ 'contact':(email,"Contact address"), 'dh':(dhgroup,"Diffie-Hellman group"), 'hash':(hash,"Hash function"), 'key-lifetime':(num,"Maximum key lifetime (ms)"), 'setup-timeout':(num,"Key setup timeout (ms)"), 'setup-retries':(num,"Maximum key setup packet retries"), 'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"), 'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"), 'restrict-nets':(networks,"Allowable networks"), 'networks':(networks,"Claimed networks"), 'pubkey':(rsakey,"RSA public site key"), 'peer':(single_ipaddr,"Tunnel peer IP address"), 'address':(address,"External contact address and port"), 'mobile':(boolean,"Site is mobile"), } def sp(name,value): "Simply output a property - the default case" return "%s %s;\n"%(name,value) # All levels support these properties global_properties={ 'contact':(lambda name,value:"# Contact email address: %s\n"%(value)), 'dh':sp, 'hash':sp, 'key-lifetime':sp, 'setup-timeout':sp, 'setup-retries':sp, 'wait-time':sp, 'renegotiate-time':sp, 'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value), } class level: "A level in the configuration hierarchy" depth=0 leaf=0 allow_properties={} require_properties={} def __init__(self,w): self.name=w[1] self.properties={} self.children={} def indent(self,w,t): w.write(" "[:t]) def prop_out(self,n): return self.allow_properties[n](n,str(self.properties[n])) def output_props(self,w,ind): for i in self.properties.keys(): if self.allow_properties[i]: self.indent(w,ind) w.write("%s"%self.prop_out(i)) def output_data(self,w,ind,np): self.indent(w,ind) w.write("%s {\n"%(self.name)) self.output_props(w,ind+2) if self.depth==1: w.write("\n"); for c in self.children.values(): c.output_data(w,ind+2,np+self.name+"/") self.indent(w,ind) w.write("};\n") class vpnlevel(level): "VPN level in the configuration hierarchy" depth=1 leaf=0 type="vpn" allow_properties=global_properties.copy() require_properties={ 'contact':"VPN admin contact address" } def __init__(self,w): level.__init__(self,w) def output_vpnflat(self,w,ind,h): "Output flattened list of site names for this VPN" self.indent(w,ind) w.write("%s {\n"%(self.name)) for i in self.children.keys(): self.children[i].output_vpnflat(w,ind+2, h+"/"+self.name+"/"+i) w.write("\n") self.indent(w,ind+2) w.write("all-sites %s;\n"% string.join(self.children.keys(),',')) self.indent(w,ind) w.write("};\n") class locationlevel(level): "Location level in the configuration hierarchy" depth=2 leaf=0 type="location" allow_properties=global_properties.copy() require_properties={ 'contact':"Location admin contact address", } def __init__(self,w): level.__init__(self,w) self.group=w[2] def output_vpnflat(self,w,ind,h): self.indent(w,ind) # The "h=h,self=self" abomination below exists because # Python didn't support nested_scopes until version 2.1 w.write("%s %s;\n"%(self.name,string.join( map(lambda x,h=h,self=self: h+"/"+x,self.children.keys()),','))) class sitelevel(level): "Site level (i.e. a leafnode) in the configuration hierarchy" depth=3 leaf=1 type="site" allow_properties=global_properties.copy() allow_properties.update({ 'address':sp, 'networks':None, 'peer':None, 'pubkey':(lambda n,v:"key %s;\n"%v), 'address':(lambda n,v:"address %s;\n"%v), 'mobile':sp, }) require_properties={ 'dh':"Diffie-Hellman group", 'contact':"Site admin contact address", 'networks':"Networks claimed by the site", 'hash':"hash function", 'peer':"Gateway address of the site", 'pubkey':"RSA public key of the site", } def __init__(self,w): level.__init__(self,w) def output_data(self,w,ind,np): self.indent(w,ind) w.write("%s {\n"%(self.name)) self.indent(w,ind+2) w.write("name \"%s\";\n"%(np+self.name)) self.output_props(w,ind+2) self.indent(w,ind+2) w.write("link netlink {\n"); self.indent(w,ind+4) w.write("routes %s;\n"%str(self.properties["networks"])) self.indent(w,ind+4) w.write("ptp-address %s;\n"%str(self.properties["peer"])) self.indent(w,ind+2) w.write("};\n") self.indent(w,ind) w.write("};\n") # Levels in the configuration file # (depth,properties) levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel} # Reserved vpn/location/site names reserved={'all-sites':None} reserved.update(keywords) reserved.update(levels) def complain(msg): "Complain about a particular input line" global complaints print ("%s line %d: "%(file,line))+msg complaints=complaints+1 def moan(msg): "Complain about something in general" global complaints print msg; complaints=complaints+1 root=level(['root','root']) # All vpns are children of this node obstack=[root] allow_defs=0 # Level above which new definitions are permitted prefix='' def set_property(obj,w): "Set a property on a configuration node" if obj.properties.has_key(w[0]): complain("%s %s already has property %s defined"% (obj.type,obj.name,w[0])) else: obj.properties[w[0]]=keywords[w[0]][0](w) def pline(i,allow_include=False): "Process a configuration file line" global allow_defs, obstack, root w=string.split(i.rstrip('\n')) if len(w)==0: return [i] keyword=w[0] current=obstack[len(obstack)-1] if keyword=='end-definitions': allow_defs=sitelevel.depth obstack=[root] return [i] if keyword=='include': if not allow_include: complain("include not permitted here") return [] if len(w) != 2: complain("include requires one argument") return [] newfile=os.path.join(os.path.dirname(file),w[1]) return pfilepath(newfile,allow_include=allow_include) if levels.has_key(keyword): # We may go up any number of levels, but only down by one newdepth=levels[keyword].depth currentdepth=len(obstack) # actually +1... if newdepth<=currentdepth: obstack=obstack[:newdepth] if newdepth>currentdepth: complain("May not go from level %d to level %d"% (currentdepth-1,newdepth)) # See if it's a new one (and whether that's permitted) # or an existing one current=obstack[len(obstack)-1] if current.children.has_key(w[1]): # Not new current=current.children[w[1]] if service and group and current.depth==2: if group!=current.group: complain("Incorrect group!") else: # New # Ignore depth check for now nl=levels[keyword](w) if nl.depth3: print "Too many arguments" sys.exit(1) pfilepath(sys.argv[1]) of=sys.stdout if len(sys.argv)>2: of=open(sys.argv[2],'w') # Sanity check section # Delete nodes where leaf=0 that have no children def live(n): "Number of leafnodes below node n" if n.leaf: return 1 for i in n.children.keys(): if live(n.children[i]): return 1 return 0 def delempty(n): "Delete nodes that have no leafnode children" for i in n.children.keys(): delempty(n.children[i]) if not live(n.children[i]): del n.children[i] delempty(root) # Check that all constraints are met (as far as I can tell # restrict-nets/networks/peer are the only special cases) def checkconstraints(n,p,ra): new_p=p.copy() new_p.update(n.properties) for i in n.require_properties.keys(): if not new_p.has_key(i): moan("%s %s is missing property %s"% (n.type,n.name,i)) for i in new_p.keys(): if not n.allow_properties.has_key(i): moan("%s %s has forbidden property %s"% (n.type,n.name,i)) # Check address range restrictions if n.properties.has_key("restrict-nets"): new_ra=ra.intersection(n.properties["restrict-nets"].set) else: new_ra=ra if n.properties.has_key("networks"): # I'd like to do this: # n.properties["networks"].set.is_subset(new_ra) # but there isn't an is_subset() method # Instead we see if we intersect with the complement of new_ra rac=new_ra.complement() i=rac.intersection(n.properties["networks"].set) if not i.is_empty(): moan("%s %s networks out of bounds"%(n.type,n.name)) if n.properties.has_key("peer"): if not n.properties["networks"].set.contains( n.properties["peer"].addr): moan("%s %s peer not in networks"%(n.type,n.name)) for i in n.children.keys(): checkconstraints(n.children[i],new_p,new_ra) checkconstraints(root,{},ipaddr.complete_set) if complaints>0: if complaints==1: print "There was 1 problem." else: print "There were %d problems."%(complaints) sys.exit(1) if service: # Put the user's input into their group file, and rebuild the main # sites file f=open(groupfiledir+"/T"+group,'w') f.write("# Section submitted by user %s, %s\n"% (user,time.asctime(time.localtime(time.time())))) f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION) for i in userinput: f.write(i) f.write("\n") f.close() os.rename(groupfiledir+"/T"+group,groupfiledir+"/R"+group) f=open(sitesfile+"-tmp",'w') f.write("# sites file autogenerated by make-secnet-sites\n") f.write("# generated %s, invoked by %s\n"% (time.asctime(time.localtime(time.time())),user)) f.write("# use make-secnet-sites to turn this file into a\n") f.write("# valid /etc/secnet/sites.conf file\n\n") for i in headerinput: f.write(i) files=os.listdir(groupfiledir) for i in files: if i[0]=='R': j=open(groupfiledir+"/"+i) f.write(j.read()) j.close() f.write("# end of sites file\n") f.close() os.rename(sitesfile+"-tmp",sitesfile) else: outputsites(of)