- Added support for InternalHosts option (ipaddress and either dns (dnspython)

or pydns (DNS) modules are now required)
This commit is contained in:
Scott Kitterman
2018-03-09 16:29:49 -05:00
parent e6021dd960
commit 4769bde19c
6 changed files with 177 additions and 18 deletions
+2
View File
@@ -11,6 +11,8 @@
added after an existing DKIM signing application to add an Ed25519 added after an existing DKIM signing application to add an Ed25519
signature (Thanks to A. Schulze for the patch) signature (Thanks to A. Schulze for the patch)
- Added support for AuthservID option - Added support for AuthservID option
- Added support for InternalHosts option (ipaddress and either dns (dnspython)
or pydns (DNS) modules are now required)
0.9.3 2018-03-02 0.9.3 2018-03-02
- Fixup csl dataset processing for single item lists - Fixup csl dataset processing for single item lists
+1 -2
View File
@@ -30,11 +30,11 @@ File dataset implemented verified
0.9.4 (Alpha) 0.9.4 (Alpha)
AuthservID implemented verified AuthservID implemented verified
InternalHosts implemented verified
0.9.5 (Beta) 0.9.5 (Beta)
Diagnostics Diagnostics
DiagnosticDirectory DiagnosticDirectory
InternalHosts
SyslogSuccess SyslogSuccess
1.0.0 1.0.0
@@ -42,7 +42,6 @@ Convert dkim-milter-python config
No additional features planned No additional features planned
Plannedataset type support: Plannedataset type support:
refile:
db:/.db db:/.db
mdb: mdb:
+3 -2
View File
@@ -69,8 +69,9 @@ class dkimMilter(Milter.Base):
self.AuthservID = self.receiver self.AuthservID = self.receiver
if hostaddr and len(hostaddr) > 0: if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0] ipaddr = hostaddr[0]
"""if iniplist(ipaddr,self.conf.internal_connect): FIXME if milterconfig['InternalHostsObj']:
self.internal_connection = True""" if milterconfig['InternalHostsObj'].match(ipaddr):
self.internal_connection = True
else: ipaddr = '' else: ipaddr = ''
self.connectip = ipaddr self.connectip = ipaddr
if self.internal_connection: if self.internal_connection:
+169 -2
View File
@@ -32,7 +32,8 @@ import urllib
import stat import stat
import dkim import dkim
import socket import socket
import ipaddress
from dnsplug import Session
# default values # default values
defaultConfigData = { defaultConfigData = {
@@ -43,7 +44,9 @@ defaultConfigData = {
'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock', 'Socket' : 'local:/var/run/dkimpy-milter/dkimpy-milter.sock',
'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid', 'PidFile' : '/var/run/dkimpy-milter/dkimpy-milter.pid',
'UserID' : 'dkimpy-milter', 'UserID' : 'dkimpy-milter',
'Canonicalization' : 'relaxed/simple' 'Canonicalization' : 'relaxed/simple',
'InternalHosts' : '127.0.0.1',
'InternalHostsObj' : False
} }
@@ -52,6 +55,167 @@ class ConfigException(Exception):
'''Exception raised when there's a configuration file error.''' '''Exception raised when there's a configuration file error.'''
pass pass
#################################
class HostsDataset(object):
'''Hold a group of host related dataset objects'''
def __init__(self, dataset):
self.dataset = []
# Self.dataset will end up being a list of DataSetItem(s).
for item in dataset:
item = item.rstrip(']')
item = item.lstrip('[')
self.dataset.append(self.DatasetItem(item))
class DatasetItem(object):
'''Individual dataset item'''
def __init__(self, item):
self.item = item
self.isipv4 = False
self.isipv4cidr = False
self.isipv6 = False
self.isipv6cidr = False
self.ishostname = False
self.isdomain = False
self.negative = False
if self.item[0] == '!':
self.item = item[1:]
self.negative = True
try:
self.item = ipaddress.ip_address(unicode(self.item, "utf-8"))
if isinstance(self.item, ipaddress.IPv4Address): self.isipv4 = True
elif isinstance(self.item, ipaddress.IPv6Address): self.isipv6 = True
except ValueError as e:
try:
self.item = ipaddress.ip_network(unicode(self.item, "utf-8"), strict=False)
if isinstance(self.item, ipaddress.IPv4Network): self.isipv4cidr = True
elif isinstance(self.item, ipaddress.IPv6Network): self.isipv6cidr = True
except ValueError as e2:
if self.item[0] == '.' and len(self.item.split('.')) > 2:
self.isdomain = True
elif len(self.item.split('.')) > 1: # It has a '.' in it
self.ishostname = True
else:
raise ConfigException('Unknown dataset item: {0}'.format(item))
def match(self, connectip):
'''Check if the connect IP is part of the dataset'''
source = ipaddress.ip_address(unicode(connectip, "utf-8"))
for item in self.dataset:
if item.isdomain or item.ishostname:
result = self.matchname(source) # Match host/domain names first
if result:
return(result)
elif item.isipv4 or item.isipv4cidr:
if isinstance(source, ipaddress.IPv4Address): # Then IPv4/6 addresses
return(self.match4(source)) # or networks depending
elif item.isipv6 or item.isipv6cidr: # on the item type and
if isinstance(source, ipaddress.IPv6Address): # connection type
return(self.match6(source))
def matchname(self, source):
'''Does source IP address relate to a domain/hostname in the dataset'''
match = False
matchone = False
negativeone = False
matchdomain = False
negativedomain = False
ptrlist = self.getptr(source)
for item in self.dataset:
if item.isdomain:
for ptr in ptrlist:
# Strip the leading '.' off the domain name so exact match works.
if item.item[1:] == ptr[-len(item.item)+1:]:
matchdomain = True
negativedomain = item.negative
elif item.ishostname:
for ptr in ptrlist:
if item.item == ptr:
matchone = True
negativeone = item.negative
if matchdomain and not negativedomain:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def getptr(self, source):
'''Get validated PTR name of IP address'''
results = []
s = Session()
ptrnames = s.dns(source.reverse_pointer, 'PTR')
for name in ptrnames:
if isinstance(source, ipaddress.IPv4Address):
ips = s.dns(name, 'A')
for ip in ips:
ip = ipaddress.IPv4Address(unicode(ip, 'UTF-8'))
if ip == source:
results.append(name)
if isinstance(source, ipaddress.IPv6Address):
ips = s.dns(name, 'AAAA')
for ip in ips:
ip = ipaddress.IPv6Address(unicode(ip, 'UTF-8'))
if ip == source:
results.append(name)
return results
def match4(self, source):
'''Is the source IP related to a IPv4 address/network in the dataset'''
match = False
matchone = False
negativeone = False
matchcidr = False
negativecidr = False
for item in self.dataset:
if item.isipv4:
if source == item.item:
matchone = True
negativeone = item.negative
elif item.isipv4cidr:
if source in item.item:
matchcidr = True
negativecidr = item.negative
if matchcidr and not negativecidr:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def match6(self, source):
'''Is the source IP realted to a IPv6 address/network in the dataset'''
match = False
matchone = False
negativeone = False
matchcidr = False
negativecidr = False
for item in self.dataset:
if item.isipv6:
if source == item.item:
matchone = True
negativeone = item.negative
elif item.isipv6cidr:
if source in item.item:
matchcidr = True
negativecidr = item.negative
if matchcidr and not negativecidr:
match = True
if matchone and not negativeone:
return True
if matchone and negativeone:
match = False
return(match)
def dump(self):
for item in self.dataset:
print 'name: {0} ip4: {1} cidr4: {2} ip6: {3} cidr6: {4} host: {5} domain: {6} negat: {7} type: {8}'.format(item.item,
item.isipv4, item.isipv4cidr, item.isipv6, item.isipv6cidr, item.ishostname, item.isdomain,
item.negative, type(item.item))
#################################################################### ####################################################################
def _processConfigFile(filename = None, configdata = None, useSyslog = 1, def _processConfigFile(filename = None, configdata = None, useSyslog = 1,
useStderr = 0): useStderr = 0):
@@ -168,6 +332,8 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
'Selector' : 'str', 'Selector' : 'str',
'SelectorEd25519': 'str', 'SelectorEd25519': 'str',
'Canonicalization' : 'str', 'Canonicalization' : 'str',
'InternalHosts' : 'dataset',
'InternalHostsObj': 'bool',
} }
# check to see if it's a file # check to see if it's a file
@@ -226,6 +392,7 @@ def _readConfigFile(path, configData = None, configGlobal = {}):
fp.close() fp.close()
try: try:
configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID']) configData['AuthservID'] = _calculate_authserv_id(configData['AuthservID'])
configData['InternalHostsObj'] = HostsDataset(configData['InternalHosts'])
except: except:
pass pass
+1 -11
View File
@@ -156,7 +156,7 @@ values, or to a file that contains them, or a database containing the data.
Some data sets require that the value contain more than one entry. How this Some data sets require that the value contain more than one entry. How this
is done depends on which data set type is used. Not all these datasets are is done depends on which data set type is used. Not all these datasets are
currently used by dkimp-milter. See currently used by dkimpy-milter. See
.B dkimpy-milter.conf(5) .B dkimpy-milter.conf(5)
for details about specific options and which dataset types they use. for details about specific options and which dataset types they use.
@@ -171,16 +171,6 @@ are ignored, and the hash ("#") character denotes the start of a comment.
If a value contains multiple entries, the entries should be separated by If a value contains multiple entries, the entries should be separated by
colons. colons.
.TP .TP
.I b)
If the string begins with "refile:", then the remainder of the string is
presumed to specify a file that contains a set of patterns, one per line,
and their associated values. The pattern is taken as the start of the line
to the first whitespace, and the portion after that whitespace is taken as
the value to be used when that pattern is matched. Patterns are simple
wildcard patterns, matching all text except that the asterisk ("*") character
is considered a wildcard. If a value contains multiple entries, the entries
should be separated by colons. [Not implemented yet]
.TP
.I c) .I c)
If the string begins with "db:" and the program was compiled with If the string begins with "db:" and the program was compiled with
Sleepycat DB support, then the remainder of the string is presumed to Sleepycat DB support, then the remainder of the string is presumed to
+1 -1
View File
@@ -55,6 +55,6 @@ setup(
(os.path.join('/lib', 'systemd', 'system'), (os.path.join('/lib', 'systemd', 'system'),
['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'), ['system/dkimpy-milter.service']),(os.path.join('/etc', 'init.d'),
['system/dkimpy-milter'])], ['system/dkimpy-milter'])],
install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl'], install_requires = ['dkimpy>=0.7', 'pymilter', 'authres>=1.1.0', 'PyNaCl', 'ipaddress', 'dns'],
zip_safe = False, zip_safe = False,
) )