feat: provide dns notify on zone updates

This commit is contained in:
s3lph 2023-04-04 22:59:39 +02:00
parent 625088abcf
commit f6a7c9628b
7 changed files with 69 additions and 5 deletions

View file

@ -1,5 +1,18 @@
# EasyWKS Changelog
<!-- BEGIN RELEASE v0.4.2 -->
## Version 0.4.2
Minor feature release
### Changes
<!-- BEGIN CHANGES 0.4.2 -->
- Add option to provide DNS NOTIFY to DANE zone replicas
<!-- END CHANGES 0.4.2-->
<!-- END RELEASE v0.4.2 -->
<!-- BEGIN RELEASE v0.4.1 -->
## Version 0.4.1

View file

@ -288,7 +288,8 @@ WantedBy=multi-user.target
If you're using EasyWKS' DANE feature, it is highly recommended to configure the SOA and NS records for each domain
you're serving. Generally you want to add NS records for all nameservers that will be serving your zone, and at least
set the MNAME and RNAME components of the SOA record:
set the MNAME and RNAME components of the SOA record. You can also configure EasyWKS to provide zone update
notifications whenever a key is modified:
```yaml
domains:
@ -298,6 +299,8 @@ domains:
- ns2.example.org.
- ns1.example.com.
- ns2.example.com.
notify:
- "2001:db8::53@10053"
soa:
mname: ns1.example.org.
rname: dnsadmin.example.org.
@ -312,9 +315,16 @@ domains:
Knot is an authoritative nameserver that supports signing a replicated zone by the secondary (replicating) nameserver.
To configure Knot to transfer the zone from EasyWKS, set up an EasyWKS remote and use it as the replication master for
DANE zones. DNSSEC signing must be enabled as well:
DANE zones. DNSSEC signing must be enabled as well. If you want Knot to be notified of zone changes, set up a notify
ACL too:
```yaml
acl:
- id: acl-easywks.example.org
address: [::1]
action: notify
remote:
- id: remote-easywks.example.org
address: [::1]@10053
@ -322,6 +332,7 @@ remote:
zone:
- domain: _openpgpkey.example.org
master: remote-easywks.example.org
acl: acl-easywks.example.org
dnssec-signing: on
dnssec-policy: ...
```

View file

@ -1,2 +1,2 @@
__version__ = '0.4.1'
__version__ = '0.4.2'

View file

@ -144,6 +144,15 @@ def _validate_dane(value):
return f'ns items must be strings, got {type(k)}'
else:
value['ns'] = ['localhost.']
if 'notify' in value:
notify = value['notify']
if not isinstance(notify, list):
return f'notify must map to a list, got {type(notify)}'
for k in notify:
if not isinstance(k, str):
return f'notify items must be strings, got {type(k)}'
else:
value['notify'] = []
def _validate_responses(value):

View file

@ -1,6 +1,5 @@
from datetime import datetime
import dns
from twisted.internet import reactor, defer
from twisted.names import dns, server, common, error
from twisted.python import util as tputil, failure

View file

@ -8,7 +8,7 @@ from pgpy import PGPKey
from .config import Config
from .crypto import create_pgp_key, privkey_to_pubkey
from .util import hash_user_id, armor_keys, split_revoked, dane_digest
from .util import hash_user_id, armor_keys, split_revoked, dane_digest, dane_notify
def _locked_read(file: str, binary: bool = False):
@ -67,6 +67,7 @@ def init_working_directory():
_locked_write(os.path.join(wdir, domain, 'hu', uid), bytes(key), binary=True)
digest = dane_digest(Config[domain].submission_address)
_locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True)
dane_notify(domain)
def read_public_key(domain, user):
@ -99,6 +100,7 @@ def write_public_key(domain, user, key, revoked):
joined = bytes(key) + b''.join([bytes(k) for k in revoked])
_locked_write(keyfile, joined, binary=True)
_locked_write(danefile, joined, binary=True)
dane_notify(domain)
def read_pending_key(domain, nonce):

View file

@ -6,10 +6,15 @@ import hashlib
import secrets
import string
import textwrap
import logging
from twisted.names import dns
from twisted.internet import reactor, defer
from pgpy import PGPKey
from pgpy.constants import SignatureType
from .config import Config
def _zrtp_base32(sha1: bytes) -> str:
# https://datatracker.ietf.org/doc/html/rfc6189#section-5.1.6
@ -83,3 +88,28 @@ def split_revoked(keys: Iterable[PGPKey]) -> Tuple[List[PGPKey], List[PGPKey]]:
break
key = [k for k in keys if k not in revoked_keys]
return key, list(revoked_keys)
def dane_notify(domain: str):
secondaries = Config[domain].dane.get('notify', [])
if len(secondaries) == 0:
return
origin = dns.domainString(f'_openpgpkey.{domain}')
# this is ugly, but has to do for now
for host in secondaries:
try:
if '@' in host:
addr, port = host.split('@', 1)
port = int(port)
else:
addr = host
port = 53
# Bind a v4 or v6 UDP client socket
proto = dns.DNSDatagramProtocol(controller=None)
reactor.listenUDP(0, proto, interface='::' if ':' in addr else '0.0.0.0')
# Assemble and send NOTIFY message
m = dns.Message(proto.pickID(), opCode=dns.OP_NOTIFY, auth=1)
m.queries = [dns.Query(origin, dns.SOA, dns.IN)]
proto.writeMessage(m, (addr, port))
except Exception:
logging.exception(f'An error occurred while attempting to notify {host}')