refactor dnscrypt parse
add test
This commit is contained in:
parent
ee37b9e95e
commit
a53b9c7a8e
10 changed files with 608 additions and 540 deletions
321
dnscrypt/__init__.py
Normal file
321
dnscrypt/__init__.py
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import base64
|
||||
import json
|
||||
|
||||
from .parser import props, LP_decode, LP_pk, VLP_hashs, VLP_bootstrap_ipi
|
||||
|
||||
|
||||
# Document
|
||||
# https://dnscrypt.info/stamps-specifications
|
||||
# https://dnscrypt.info/protocol
|
||||
# https://github.com/DNSCrypt/dnscrypt-resolvers/blob/21fcbaf858112c63fed2a504714cc829bd654483/utils/format.py#L101-L141
|
||||
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
if b[0] == 0x01:
|
||||
return DNSCrypt.parse(stamp)
|
||||
elif b[0] == 0x02:
|
||||
return DNSoverHTTPS.parse(stamp)
|
||||
elif b[0] == 0x03:
|
||||
return DNSoverTLS.parse(stamp)
|
||||
elif b[0] == 0x04:
|
||||
return DNSoverQUIC.parse(stamp)
|
||||
elif b[0] == 0x05:
|
||||
return ObliviousDoH.parse(stamp)
|
||||
elif b[0] == 0x81:
|
||||
return DNSCryptRelay.parse(stamp)
|
||||
elif b[0] == 0x85:
|
||||
return ObliviousDoHRelay.parse(stamp)
|
||||
elif b[0] == 0x00:
|
||||
return PlainDNS.parse(stamp)
|
||||
|
||||
|
||||
class Base:
|
||||
def to_json(self):
|
||||
return json.dumps(
|
||||
self,
|
||||
default=lambda o: o.__dict__
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + '<' + self.to_json() + '>'
|
||||
|
||||
|
||||
class DNSCrypt(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
pk: str = None
|
||||
provider: str = None
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x01
|
||||
i = 0
|
||||
if b[i] != 0x01:
|
||||
raise ValueError("DNSCrypt: 0x01")
|
||||
i = i + 1
|
||||
|
||||
parsed = DNSCrypt()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr [:port])
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
# LP(pk)
|
||||
i, parsed.pk = LP_pk(b, i)
|
||||
|
||||
# LP(providerName)
|
||||
i, parsed.provider = LP_decode(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class DNSoverHTTPS(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
hashs: list[str] = []
|
||||
hostname: str = None
|
||||
path: str = None
|
||||
bootstrap_ipi: list[str] = []
|
||||
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x02
|
||||
i = 0
|
||||
if b[i] != 0x02:
|
||||
raise ValueError("DNSoverHTTPS: 0x02")
|
||||
i = i + 1
|
||||
|
||||
parsed = DNSoverHTTPS()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
# VLP(hash1, hash2, ...hashn)
|
||||
i, parsed.hashs = VLP_hashs(b, i)
|
||||
|
||||
# LP(hostname [:port])
|
||||
i, parsed.hostname = LP_decode(b, i)
|
||||
|
||||
# LP(path)
|
||||
i, parsed.path = LP_decode(b, i)
|
||||
|
||||
# VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
|
||||
if i < len(b):
|
||||
i, parsed.bootstrap_ipi = VLP_bootstrap_ipi(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class DNSoverTLS(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
hashs: list[str] = []
|
||||
hostname: str = None
|
||||
bootstrap_ipi: list[str] = []
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x03
|
||||
i = 0
|
||||
if b[i] != 0x03:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = DNSoverTLS()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
# VLP(hash1, hash2, ...hashn)
|
||||
i, parsed.hashs = VLP_hashs(b, i)
|
||||
|
||||
# LP(hostname[:port])
|
||||
i, parsed.hostname = LP_decode(b, i)
|
||||
|
||||
# VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
|
||||
if i < len(b):
|
||||
i, parsed.bootstrap_ipi = VLP_bootstrap_ipi(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class DNSoverQUIC(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
hashs: list[str] = []
|
||||
hostname: str = None
|
||||
bootstrap_ipi: list[str] = []
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x04
|
||||
i = 0
|
||||
if b[i] != 0x04:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = DNSoverQUIC()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
# VLP(hash1, hash2, ...hashn)
|
||||
i, parsed.hashs = VLP_hashs(b, i)
|
||||
|
||||
# LP(hostname[:port])
|
||||
i, parsed.hostname = LP_decode(b, i)
|
||||
|
||||
# VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
|
||||
if i < len(b):
|
||||
i, parsed.bootstrap_ipi = VLP_bootstrap_ipi(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class ObliviousDoH(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
hostname: str = None
|
||||
path: str = None
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x05
|
||||
i = 0
|
||||
if b[i] != 0x05:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = ObliviousDoH()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(hostname [:port])
|
||||
i, parsed.hostname = LP_decode(b, i)
|
||||
|
||||
# LP(path)
|
||||
i, parsed.path = LP_decode(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class DNSCryptRelay(Base):
|
||||
addr: str = None
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x81
|
||||
i = 0
|
||||
if b[i] != 0x81:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = DNSCryptRelay()
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class ObliviousDoHRelay(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
hashs: list[str] = []
|
||||
hostname: str = None
|
||||
path: str = None
|
||||
bootstrap_ipi: list[str] = []
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x85
|
||||
i = 0
|
||||
if b[i] != 0x85:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = ObliviousDoHRelay()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
# VLP(hash1, hash2, ...hashn)
|
||||
i, parsed.hashs = VLP_hashs(b, i)
|
||||
|
||||
# LP(hostname [:port])
|
||||
i, parsed.hostname = LP_decode(b, i)
|
||||
|
||||
# LP(path)
|
||||
i, parsed.path = LP_decode(b, i)
|
||||
|
||||
# VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
|
||||
if i < len(b):
|
||||
i, parsed.bootstrap_ipi = VLP_bootstrap_ipi(b, i)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
class PlainDNS(Base):
|
||||
dnssec: bool = False
|
||||
nolog: bool = False
|
||||
nofilter: bool = False
|
||||
addr: str = None
|
||||
|
||||
@staticmethod
|
||||
def parse(stamp: str):
|
||||
b = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
|
||||
|
||||
# 0x00
|
||||
i = 0
|
||||
if b[i] != 0x00:
|
||||
raise ValueError()
|
||||
i = i + 1
|
||||
|
||||
parsed = PlainDNS()
|
||||
|
||||
# props
|
||||
i, parsed.dnssec, parsed.nolog, parsed.nofilter = props(b, i)
|
||||
|
||||
# LP(addr)
|
||||
i, parsed.addr = LP_decode(b, i)
|
||||
|
||||
return parsed
|
||||
91
dnscrypt/parser.py
Normal file
91
dnscrypt/parser.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
def props(b: bytes, i: int):
|
||||
'''
|
||||
``props`` is a little-endian 64 bit value that represents informal properties about the resolver. It is a logical OR
|
||||
combination of the following values:
|
||||
|
||||
- ``1``: the server supports DNSSEC
|
||||
- ``2``: the server doesn’t keep logs
|
||||
- ``4``: the server doesn’t intentionally block domains
|
||||
|
||||
For example, a server that supports DNSSEC, stores logs, but doesn’t block anything on its own should set ``props``
|
||||
as the following 8 bytes sequence: ``[ 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]``.
|
||||
'''
|
||||
props = b[i]
|
||||
dnssec = not not ((props >> 0) & 1)
|
||||
nolog = not not ((props >> 1) & 1)
|
||||
nofilter = not not ((props >> 2) & 1)
|
||||
i = i + 8
|
||||
return i, dnssec, nolog, nofilter
|
||||
|
||||
|
||||
def LP(b: bytes, i: int):
|
||||
'''
|
||||
``a || b`` is the concatenation of ``a`` and ``b``
|
||||
|
||||
``len(x)`` is a single byte representation of the length of ``x``, in bytes. Strings don’t have to be zero-terminated
|
||||
and do not require invidual encoding.
|
||||
|
||||
``LP(x)`` is ``len(x) || x``, i.e ``x`` prefixed by its length.
|
||||
'''
|
||||
x_len = b[i]
|
||||
i = i + 1
|
||||
x = b[i:i + x_len]
|
||||
i = i + x_len
|
||||
return i, x
|
||||
|
||||
|
||||
def LP_decode(b: bytes, i: int, encoding='utf-8'):
|
||||
i, x = LP(b, i)
|
||||
return i, x.decode(encoding)
|
||||
|
||||
|
||||
def LP_pk(b: bytes, i: int):
|
||||
i, x = LP(b, i)
|
||||
if len(x) != 32:
|
||||
raise ValueError("LP(pk)")
|
||||
hpk = x.hex().upper()
|
||||
hpks = []
|
||||
for j in range(0, 16):
|
||||
hpks.append(hpk[j * 4: j * 4 + 4])
|
||||
pk = ":".join(hpks)
|
||||
return i, pk
|
||||
|
||||
|
||||
def VLP(b: bytes, i: int):
|
||||
'''
|
||||
``a | b`` is the result of the logical ``OR`` operation between ``a`` and ``b``.
|
||||
|
||||
``vlen(x)`` is equal to ``len(x)`` if ``x`` is the last element of a set, and ``0x80 | len(x)`` if there are more
|
||||
elements in the set.
|
||||
|
||||
``VLP(x1, x2, ...xn)`` encodes a set, as ``vlen(x1) || x1 || vlen(x2) || x2 ... || vlen(xn) || xn``.
|
||||
Since ``vlen(xn) == len(xn)`` (length of the last element doesn’t have the high bit set), for a set with a single
|
||||
element, we have ``VLP(x) == LP(x)``.
|
||||
'''
|
||||
xx = []
|
||||
last_element = False
|
||||
while True:
|
||||
if b[i] & 0x80 == 0:
|
||||
last_element = True
|
||||
x_len = b[i]
|
||||
else:
|
||||
x_len = b[i] ^ 0x80
|
||||
i = i + 1
|
||||
if x_len > 0:
|
||||
x = b[i:i + x_len]
|
||||
xx.append(x)
|
||||
i = i + x_len
|
||||
if last_element:
|
||||
return i, xx
|
||||
|
||||
|
||||
def VLP_hashs(b: bytes, i: int):
|
||||
i, xx = VLP(b, i)
|
||||
hashs = list(map(lambda x: x.hex(), xx))
|
||||
return i, hashs
|
||||
|
||||
|
||||
def VLP_bootstrap_ipi(b: bytes, i: int):
|
||||
i, xx = VLP(b, i)
|
||||
bootstrap_ipi = list(map(lambda x: x.decode("utf-8"), xx))
|
||||
return i, bootstrap_ipi
|
||||
Loading…
Add table
Add a link
Reference in a new issue