X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=secnet.git;a=blobdiff_plain;f=make-secnet-sites;h=8e3ec7360f0379d40d4dda703965a5217281225a;hp=5d6de21a8ce591604e013486fcacbdfe20212643;hb=45cfab8ca7db61ce4677e2e77e76b9266c57ab12;hpb=469fd1d95b2528212a46b155cb115c078de4228f diff --git a/make-secnet-sites b/make-secnet-sites index 5d6de21..8e3ec73 100755 --- a/make-secnet-sites +++ b/make-secnet-sites @@ -1,5 +1,5 @@ #! /usr/bin/env python -# Copyright (C) 2001 Stephen Early +# 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 @@ -53,217 +53,319 @@ import string import time import sys import os +import getopt +# The ipaddr library is installed as part of secnet 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: +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.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()),",") + 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 out(self): - return 'dh diffie-hellman("%s","%s");'%(self.mod,self.gen) + 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 out(self): - return 'hash %s;'%(self.ht) + def __str__(self): + return '%s'%(self.ht) class email: + "An email address" def __init__(self,w): self.addr=w[1] - def out(self): - return '# Contact email address: <%s>'%(self.addr) + def __str__(self): + return '<%s>'%(self.addr) class num: + "A decimal number" def __init__(self,w): - self.what=w[0] self.n=string.atol(w[1]) - def out(self): - return '%s %d;'%(self.what,self.n) + def __str__(self): + return '%d'%(self.n) class address: + "A DNS name and UDP port number" 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) + 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 out(self): - return 'key rsa-public("%s","%s");'%(self.e,self.n) - -class mobileoption: + 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") +} + +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) + }) + require_properties={ + 'dh':"Diffie-Hellman group", + 'contact':"Site admin contact address", + 'address':"Site external access 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): - self.w=w - def out(self): - return '# netlink-options "soft";' + 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 -# 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) +root=level(['root','root']) # All vpns are children of this node +obstack=[root] +allow_defs=0 # Level above which new definitions are permitted + +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) -# Process a line of configuration file def pline(i): - global allow_defs, group, current_vpn, current_location, current_object + "Process a configuration file line" + global allow_defs, obstack, root w=string.split(i) if len(w)==0: return keyword=w[0] + current=obstack[len(obstack)-1] if keyword=='end-definitions': - allow_defs=0 - current_vpn=None - current_location=None - current_object=None + allow_defs=sitelevel.depth + obstack=[root] return - if keyword=='vpn': - if vpns.has_key(w[1]): - current_vpn=vpns[w[1]] - current_object=current_vpn + 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: - if allow_defs: - current_vpn=vpn(w[1]) - vpns[w[1]]=current_vpn - current_object=current_vpn - else: - complain("no new VPN definitions allowed") + # New + # Ignore depth check for now + nl=levels[keyword](w) + if nl.depth0: if complaints==1: print "There was 1 problem." @@ -471,16 +517,16 @@ if service: 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) + 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.py\n") + 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.py to turn this file into a\n") + 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)