...
 
Commits (7)
......@@ -13,7 +13,7 @@ Server has configurable cache backend which allows to store cached STS policies
## Requirements
* Postfix 2.10 and later
* Postfix 3.4+ (or Postfix 2.10+ if missing Postfix SNI feature is tolerable. In that case you have to set zone option `require_sni` to `false` in MTA-STS daemon config)
* Python 3.5.3+ (see ["Systems without Python 3.5+"](#systems-without-python-35) below if you haven't one, or use Docker installation method)
* aiodns
* aiohttp
......
......@@ -55,6 +55,7 @@ It is unaffected by `cache_grace` and vice versa. Default: 86400
* *strict_testing*: (_bool_) enforce policy for testing domains
* *timeout*: (_int_) network operations timeout for resolver in that zone
* *require_sni*: (_bool_) add option `servername=hostname` to policy responses to make Postfix send SNI in TLS handshake as required by RFC 8461. Requires Postfix version 3.4+. Default: true
*zones*::
......
......@@ -18,3 +18,4 @@ PROACTIVE_FETCH_INTERVAL = 86400
PROACTIVE_FETCH_CONCURRENCY_LIMIT = 100
PROACTIVE_FETCH_GRACE_RATIO = 2.0
USER_AGENT = "postfix-mta-sts-resolver"
REQUIRE_SNI = True
......@@ -14,7 +14,7 @@ from .base_cache import CacheEntry
from . import netstring
ZoneEntry = collections.namedtuple('ZoneEntry', ('strict', 'resolver'))
ZoneEntry = collections.namedtuple('ZoneEntry', ('strict', 'resolver', 'require_sni'))
# pylint: disable=too-many-instance-attributes
......@@ -37,11 +37,13 @@ class STSSocketmapResponder:
# Construct configurations and resolvers for every socketmap name
self._default_zone = ZoneEntry(cfg["default_zone"]["strict_testing"],
STSResolver(loop=loop,
timeout=cfg["default_zone"]["timeout"]))
timeout=cfg["default_zone"]["timeout"]),
cfg["default_zone"]["require_sni"])
self._zones = dict((k, ZoneEntry(zone["strict_testing"],
STSResolver(loop=loop,
timeout=zone["timeout"])))
timeout=zone["timeout"]),
zone["require_sni"]))
for k, zone in cfg["zones"].items())
self._cache = cache
......@@ -220,6 +222,8 @@ class STSSocketmapResponder:
assert cached.pol_body['mx'], "Empty MX list for restrictive policy!"
mxlist = [mx.lstrip('*') for mx in set(cached.pol_body['mx'])]
resp = "OK secure match=" + ":".join(mxlist)
if zone_cfg.require_sni:
resp += " servername=hostname"
return netstring.encode(resp.encode('utf-8'))
else:
return netstring.encode(b'NOTFOUND ')
......
......@@ -113,6 +113,7 @@ def populate_cfg_defaults(cfg):
def populate_zone(zone):
zone['timeout'] = zone.get('timeout', defaults.TIMEOUT)
zone['strict_testing'] = zone.get('strict_testing', defaults.STRICT_TESTING)
zone['require_sni'] = zone.get('require_sni', defaults.REQUIRE_SNI)
return zone
if 'default_zone' not in cfg:
......
......@@ -7,7 +7,7 @@ with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
long_description = f.read() # pylint: disable=invalid-name
setup(name='postfix_mta_sts_resolver',
version='0.8.2',
version='1.0.0',
description='Daemon which provides TLS client policy for Postfix '
'via socketmap, according to domain MTA-STS policy',
url='https://github.com/Snawoot/postfix-mta-sts-resolver',
......
name: postfix-mta-sts-resolver
version: '0.8.2'
version: '1.0.0'
summary: Policy-server for Postfix which handles MTA-STS resolving
description: |
Daemon which provides TLS client policy for Postfix via socketmap,
......
test good.loc OK secure match=mail.loc
test [good.loc] OK secure match=mail.loc
test [good.loc]:123 OK secure match=mail.loc
test good.loc:123 OK secure match=mail.loc
test2 good.loc OK secure match=mail.loc
test good.loc. OK secure match=mail.loc
test good.loc OK secure match=mail.loc servername=hostname
test [good.loc] OK secure match=mail.loc servername=hostname
test [good.loc]:123 OK secure match=mail.loc servername=hostname
test good.loc:123 OK secure match=mail.loc servername=hostname
test2 good.loc OK secure match=mail.loc servername=hostname
test good.loc. OK secure match=mail.loc servername=hostname
test .good.loc NOTFOUND
test valid-none.loc NOTFOUND
test testing.loc NOTFOUND
......
test good.loc OK secure match=mail.loc
test [good.loc] OK secure match=mail.loc
test [good.loc]:123 OK secure match=mail.loc
test good.loc:123 OK secure match=mail.loc
test2 good.loc OK secure match=mail.loc
test good.loc. OK secure match=mail.loc
test .good.loc NOTFOUND
test valid-none.loc NOTFOUND
test testing.loc NOTFOUND
test no-record.loc NOTFOUND
test [no-record.loc] NOTFOUND
test .no-record.loc NOTFOUND
test [1.2.3.4] NOTFOUND
test [a:bb:ccc::dddd]:123 NOTFOUND
test bad-record1.loc NOTFOUND
test bad-record2.loc NOTFOUND
test bad-policy1.loc NOTFOUND
test bad-policy2.loc NOTFOUND
test bad-policy3.loc NOTFOUND
test bad-cert1.loc NOTFOUND
test bad-cert2.loc NOTFOUND
test good.loc OK secure match=mail.loc
test [good.loc] OK secure match=mail.loc
test [good.loc]:123 OK secure match=mail.loc
test good.loc:123 OK secure match=mail.loc
test2 good.loc OK secure match=mail.loc
test good.loc. OK secure match=mail.loc
test good.loc OK secure match=mail.loc servername=hostname
test [good.loc] OK secure match=mail.loc servername=hostname
test [good.loc]:123 OK secure match=mail.loc servername=hostname
test good.loc:123 OK secure match=mail.loc servername=hostname
test2 good.loc OK secure match=mail.loc servername=hostname
test good.loc. OK secure match=mail.loc servername=hostname
test .good.loc NOTFOUND
test valid-none.loc NOTFOUND
test testing.loc OK secure match=mail.loc
test testing.loc OK secure match=mail.loc servername=hostname
test no-record.loc NOTFOUND
test [no-record.loc] NOTFOUND
test .no-record.loc NOTFOUND
......
......@@ -184,7 +184,7 @@ async def test_fast_expire(responder):
await asyncio.sleep(2)
writer.write(netstring.encode(b'test fast-expire.loc'))
answer_b = await answer()
assert answer_a == answer_b == b'OK secure match=mail.loc'
assert answer_a == answer_b == b'OK secure match=mail.loc servername=hostname'
finally:
writer.close()
......
import sys
import asyncio
import itertools
import socket
import pytest
from postfix_mta_sts_resolver import netstring
from postfix_mta_sts_resolver.responder import STSSocketmapResponder
import postfix_mta_sts_resolver.utils as utils
from async_generator import yield_, async_generator
from testdata import load_testdata
@pytest.fixture(scope="module")
@async_generator
async def responder(event_loop):
import postfix_mta_sts_resolver.utils as utils
cfg = utils.populate_cfg_defaults({"default_zone": {"require_sni": False}})
cfg["zones"]["test2"] = cfg["default_zone"]
cfg["port"] = 28461
cache = utils.create_cache(cfg['cache']['type'],
cfg['cache']['options'])
await cache.setup()
resp = STSSocketmapResponder(cfg, event_loop, cache)
await resp.start()
result = resp, cfg['host'], cfg['port']
await yield_(result)
await resp.stop()
await cache.teardown()
buf_sizes = [4096, 128, 16, 1]
reqresps = list(load_testdata('refdata_nosni'))
@pytest.mark.parametrize("params", tuple(itertools.product(reqresps, buf_sizes)))
@pytest.mark.asyncio
@pytest.mark.timeout(5)
async def test_responder(responder, params):
(request, response), bufsize = params
resp, host, port = responder
reader, writer = await asyncio.open_connection(host, port)
stream_reader = netstring.StreamReader()
string_reader = stream_reader.next_string()
try:
writer.write(netstring.encode(request))
res = b''
while True:
try:
part = string_reader.read()
except netstring.WantRead:
data = await reader.read(bufsize)
assert data
stream_reader.feed(data)
else:
if not part:
break
res += part
assert res == response
finally:
writer.close()
......@@ -122,6 +122,6 @@ async def test_fast_expire(responder):
await asyncio.sleep(2)
writer.write(netstring.encode(b'test fast-expire.loc'))
answer_b = await answer()
assert answer_a == answer_b == b'OK secure match=mail.loc'
assert answer_a == answer_b == b'OK secure match=mail.loc servername=hostname'
finally:
writer.close()