#! /usr/bin/env python # Copyright (C) 2001 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 sys.path.append("/usr/local/share/secnet") sys.path.append("/usr/share/secnet") import ipaddr VERSION="0.1.10" class vpn: def __init__(self,name): self.name=name self.allow_defs=0 self.locations={} self.defs={} class location: def __init__(self,name,vpn): self.group=None self.name=name self.allow_defs=1 self.vpn=vpn self.sites={} self.defs={} class site: def __init__(self,name,location): self.name=name self.allow_defs=1 self.location=location self.defs={} class nets: def __init__(self,w): self.w=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 subsetof(self,s): # I'd like to do this: # return self.set.is_subset(s) # but there isn't an is_subset() method # Instead we see if we intersect with the complement of s sc=s.set.complement() i=sc.intersection(self.set) return i.is_empty() def out(self): if (self.w[0]=='restrict-nets'): pattern="# restrict-nets %s;" else: pattern="link netlink { routes %s; };" return pattern%string.join(map(lambda x:'"%s/%s"'%(x.ip_str(), x.mask.netmask_bits_str), self.set.as_list_of_networks()),",") class dhgroup: def __init__(self,w): self.mod=w[1] self.gen=w[2] def out(self): return 'dh diffie-hellman("%s","%s");'%(self.mod,self.gen) class hash: def __init__(self,w): self.ht=w[1] if (self.ht!='md5' and self.ht!='sha1'): complain("unknown hash type %s"%(self.ht)) def out(self): return 'hash %s;'%(self.ht) class email: def __init__(self,w): self.addr=w[1] def out(self): return '# Contact email address: <%s>'%(self.addr) class num: def __init__(self,w): self.what=w[0] self.n=string.atol(w[1]) def out(self): return '%s %d;'%(self.what,self.n) class address: def __init__(self,w): self.w=w self.adr=w[1] self.port=string.atoi(w[2]) if (self.port<1 or self.port>65535): complain("invalid port number") def out(self): return 'address "%s"; port %d;'%(self.adr,self.port) class rsakey: def __init__(self,w): self.l=string.atoi(w[1]) self.e=w[2] self.n=w[3] def out(self): return 'key rsa-public("%s","%s");'%(self.e,self.n) class mobileoption: def __init__(self,w): self.w=w def out(self): return '# netlink-options "soft";' def complain(msg): global complaints print ("%s line %d: "%(file,line))+msg complaints=complaints+1 def moan(msg): global complaints print msg; complaints=complaints+1 # We don't allow redefinition of properties (because that would allow things # like restrict-nets to be redefined, which would be bad) def set(obj,defs,w): if (obj.allow_defs | allow_defs): if (obj.defs.has_key(w[0])): complain("%s is already defined"%(w[0])) else: t=defs[w[0]] obj.defs[w[0]]=t(w) # Process a line of configuration file def pline(i): global allow_defs, group, current_vpn, current_location, current_object w=string.split(i) if len(w)==0: return keyword=w[0] if keyword=='end-definitions': allow_defs=0 current_vpn=None current_location=None current_object=None return if keyword=='vpn': if vpns.has_key(w[1]): current_vpn=vpns[w[1]] current_object=current_vpn else: if allow_defs: current_vpn=vpn(w[1]) vpns[w[1]]=current_vpn current_object=current_vpn else: complain("no new VPN definitions allowed") return if (current_vpn==None): complain("no VPN defined yet") return # Keywords that can apply at all levels if mldefs.has_key(w[0]): set(current_object,mldefs,w) return if keyword=='location': if (current_vpn.locations.has_key(w[1])): current_location=current_vpn.locations[w[1]] current_object=current_location if (group and not allow_defs and current_location.group!=group): complain(("must be group %s to access "+ "location %s")%(current_location.group, w[1])) else: if allow_defs: if reserved.has_key(w[1]): complain("reserved location name") return current_location=location(w[1],current_vpn) current_vpn.locations[w[1]]=current_location current_object=current_location else: complain("no new location definitions allowed") return if (current_location==None): complain("no locations defined yet") return if keyword=='group': current_location.group=w[1] return if keyword=='site': if (current_location.sites.has_key(w[1])): current_object=current_location.sites[w[1]] else: if reserved.has_key(w[1]): complain("reserved site name") return current_object=site(w[1],current_location) current_location.sites[w[1]]=current_object return if keyword=='endsite': if isinstance(current_object,site): current_object=current_object.location else: complain("not currently defining a site") return # Keywords that can only apply to sites if isinstance(current_object,site): if sitedefs.has_key(w[0]): set(current_object,sitedefs,w) return else: if sitedefs.has_key(w[0]): complain("keyword '%s' can only be used in the " "context of a site definition"%(w[0])) return complain("unknown keyword '%s'"%(w[0])) def pfile(name,lines): global file,line file=name line=0 for i in lines: line=line+1 if (i[0]=='#'): continue if (i[len(i)-1]=='\n'): i=i[:len(i)-1] # strip trailing LF pline(i) def outputsites(w): w.write("# secnet sites file autogenerated by make-secnet-sites.py " +"version %s\n"%VERSION) w.write("# %s\n\n"%time.asctime(time.localtime(time.time()))) # Raw VPN data section of file w.write("vpn-data {\n") for i in vpns.values(): w.write(" %s {\n"%i.name) for d in i.defs.values(): w.write(" %s\n"%d.out()) w.write("\n") for l in i.locations.values(): w.write(" %s {\n"%l.name) for d in l.defs.values(): w.write(" %s\n"%d.out()) for s in l.sites.values(): w.write(" %s {\n"%s.name) w.write(' name "%s/%s/%s";\n'% (i.name,l.name,s.name)) for d in s.defs.values(): w.write(" %s\n"%d.out()) w.write(" };\n") w.write(" };\n") w.write(" };\n") w.write("};\n") # Per-VPN flattened lists w.write("vpn {\n") for i in vpns.values(): w.write(" %s {\n"%(i.name)) for l in i.locations.values(): tmpl="vpn-data/%s/%s/%%s"%(i.name,l.name) slist=[] for s in l.sites.values(): slist.append(tmpl%s.name) w.write(" %s %s;\n"%(l.name,string.join(slist,","))) w.write("\n all-sites %s;\n"% string.join(i.locations.keys(),",")) w.write(" };\n") w.write("};\n") # Flattened list of sites w.write("all-sites %s;\n"%string.join(map(lambda x:"vpn/%s/all-sites"% x,vpns.keys()),",")) # Are we being invoked from userv? service=0 # If we are, which group does the caller want to modify? group=None vpns={} allow_defs=1 current_vpn=None current_location=None current_object=None line=0 file=None complaints=0 # Things that can be defined at any level mldefs={ 'dh':dhgroup, 'hash':hash, 'contact':email, 'key-lifetime':num, 'setup-retries':num, 'setup-timeout':num, 'wait-time':num, 'renegotiate-time':num, 'restrict-nets':nets } # Things that can only be defined for sites sitedefs={ 'address':address, 'networks':nets, 'pubkey':rsakey, 'mobile':mobileoption } # Reserved vpn/location/site names reserved={'all-sites':None} reserved.update(mldefs) reserved.update(sitedefs) # Each site must have the following defined at some level: required={ 'dh':"Diffie-Hellman group", 'networks':"network list", 'pubkey':"public key", 'hash':"hash function" } if len(sys.argv)<2: pfile("stdin",sys.stdin.readlines()) of=sys.stdout else: if sys.argv[1]=='-u': if len(sys.argv)!=6: print "Wrong number of arguments" sys.exit(1) service=1 header=sys.argv[2] groupfiledir=sys.argv[3] sitesfile=sys.argv[4] group=sys.argv[5] if not os.environ.has_key("USERV_USER"): print "Environment variable USERV_USER not found" sys.exit(1) user=os.environ["USERV_USER"] # Check that group is in USERV_GROUP if not os.environ.has_key("USERV_GROUP"): print "Environment variable USERV_GROUP not found" sys.exit(1) ugs=os.environ["USERV_GROUP"] ok=0 for i in string.split(ugs): if group==i: ok=1 if not ok: print "caller not in group %s"%group sys.exit(1) f=open(header) headerinput=f.readlines() f.close() pfile(header,headerinput) userinput=sys.stdin.readlines() pfile("user input",userinput) else: if len(sys.argv)>3: print "Too many arguments" sys.exit(1) f=open(sys.argv[1]) pfile(sys.argv[1],f.readlines()) f.close() of=sys.stdout if len(sys.argv)>2: of=open(sys.argv[2],'w') # Sanity check section # Delete locations that have no sites defined for i in vpns.values(): for l in i.locations.keys(): if (len(i.locations[l].sites.values())==0): del i.locations[l] # Delete VPNs that have no locations with sites defined for i in vpns.keys(): if (len(vpns[i].locations.values())==0): del vpns[i] # Check all sites for i in vpns.values(): if i.defs.has_key('restrict-nets'): vr=i.defs['restrict-nets'] else: vr=None for l in i.locations.values(): if l.defs.has_key('restrict-nets'): lr=l.defs['restrict-nets'] if (not lr.subsetof(vr)): moan("location %s/%s restrict-nets is invalid"% (i.name,l.name)) else: lr=vr for s in l.sites.values(): sn="%s/%s/%s"%(i.name,l.name,s.name) for r in required.keys(): if (not (s.defs.has_key(r) or l.defs.has_key(r) or i.defs.has_key(r))): moan("site %s missing parameter %s"% (sn,r)) if s.defs.has_key('restrict-nets'): sr=s.defs['restrict-nets'] if (not sr.subsetof(lr)): moan("site %s restrict-nets not valid"% sn) else: sr=lr if not s.defs.has_key('networks'): continue nets=s.defs['networks'] if (not nets.subsetof(sr)): moan("site %s networks exceed restriction"%sn) 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.py 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.py\n") f.write("# generated %s, invoked by %s\n"% (time.asctime(time.localtime(time.time())),user)) f.write("# use make-secnet-sites.py 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)