diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9dcd3d9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
+test:
+	coverage run -m unittest discover testing; \
+	coverage report; \
+	coverage html
+clean:
+	rm -r .coverage htmlcov
\ No newline at end of file
diff --git a/dnscrypt/__init__.py b/dnscrypt/__init__.py
new file mode 100644
index 0000000..e7701e2
--- /dev/null
+++ b/dnscrypt/__init__.py
@@ -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
diff --git a/dnscrypt/parser.py b/dnscrypt/parser.py
new file mode 100644
index 0000000..64fdbd6
--- /dev/null
+++ b/dnscrypt/parser.py
@@ -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
diff --git a/dnscrypt_to_smartdns.py b/dnscrypt_to_smartdns.py
index fba44d1..a3f65e5 100644
--- a/dnscrypt_to_smartdns.py
+++ b/dnscrypt_to_smartdns.py
@@ -1,8 +1,8 @@
-import subprocess
 import logging
+import subprocess
 from urllib.request import urlopen
 
-from utils.dnsstamps import parse, DNSoverHTTPS
+from dnscrypt import parse, DNSoverHTTPS
 
 SMARTDNS_GFW_CONF_FILE = '/etc/smartdns/conf.d/gfw.conf'
 
@@ -80,4 +80,4 @@ def main():
 
 
 if __name__ == '__main__':
-    main()
\ No newline at end of file
+    main()
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..a4bac9d
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,80 @@
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+
+[[package]]
+name = "coverage"
+version = "7.6.10"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+    { file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78" },
+    { file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c" },
+    { file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a" },
+    { file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165" },
+    { file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988" },
+    { file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5" },
+    { file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3" },
+    { file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5" },
+    { file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244" },
+    { file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e" },
+    { file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3" },
+    { file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43" },
+    { file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132" },
+    { file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f" },
+    { file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994" },
+    { file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99" },
+    { file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd" },
+    { file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377" },
+    { file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8" },
+    { file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609" },
+    { file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853" },
+    { file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078" },
+    { file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0" },
+    { file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50" },
+    { file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022" },
+    { file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b" },
+    { file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0" },
+    { file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852" },
+    { file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359" },
+    { file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247" },
+    { file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9" },
+    { file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b" },
+    { file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690" },
+    { file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18" },
+    { file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c" },
+    { file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd" },
+    { file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e" },
+    { file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694" },
+    { file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6" },
+    { file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e" },
+    { file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe" },
+    { file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273" },
+    { file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8" },
+    { file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098" },
+    { file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb" },
+    { file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0" },
+    { file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf" },
+    { file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2" },
+    { file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312" },
+    { file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d" },
+    { file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a" },
+    { file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27" },
+    { file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4" },
+    { file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f" },
+    { file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25" },
+    { file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315" },
+    { file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90" },
+    { file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d" },
+    { file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18" },
+    { file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59" },
+    { file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f" },
+    { file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23" },
+]
+
+[package.extras]
+toml = ["tomli"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.13"
+content-hash = "bb288bb587807b6be40745ef9083750cdf5bf09de9a5d44fb95d778b7aaa69b3"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..dbe3fcf
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "openwrt-dns"
+version = "0.1.0"
+description = ""
+authors = ["bgme <i@bgme.me>"]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.13"
+
+
+[tool.poetry.group.dev.dependencies]
+coverage = "^7.6.10"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/testing/test_dnscrypt.py b/testing/test_dnscrypt.py
new file mode 100644
index 0000000..5f8a706
--- /dev/null
+++ b/testing/test_dnscrypt.py
@@ -0,0 +1,57 @@
+import unittest
+
+from dnscrypt import parse, DNSCrypt, DNSoverHTTPS, DNSoverTLS, DNSoverQUIC, ObliviousDoH, DNSCryptRelay, \
+    ObliviousDoHRelay, PlainDNS
+
+
+def comman_parse_test(test_case, stamp, instance, except_result_json):
+    result = parse(stamp)
+    test_case.assertIsInstance(result, instance)
+    test_case.assertEqual(except_result_json, result.to_json())
+
+
+class TestDNSCryptStampParse(unittest.TestCase):
+
+    def test_DNSCrypt(self):
+        stamp = "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": false, "addr": "94.140.14.15:5443", "pk": "B831:5DD7:B14B:6EE3:20A4:70DC:2ED6:B1AA:398C:C9E5:86F8:5D45:45D6:B8C9:B500:5ABA", "provider": "2.dnscrypt.family.ns1.adguard.com"}'
+        comman_parse_test(self, stamp, DNSCrypt, except_result_json)
+
+    def test_DNSoverHTTPS(self):
+        stamp = "sdns://AgMAAAAAAAAADjE2My40Ny4xMTcuMTc2oMwQYNOcgym2K2-8fQ1t-TCYabmB5-Y5LVzY-kCPTYDmoPf1ryiAHod9ffOivij-FJ8ydKftKfE2_VA845jLqAsNoLNeBZUM-9gln5N1uhAYcLjDxMDsWlKXV-YxZ-neJqnooEROvWe7g_iAezkh6TiskXi4gr1QqtsRIx8ETPXwjffOoOZEumlj4zX-dly5l2sSsQ61QpS0JHd2TMs6OsyjrLL8ICquP7e_BeTIHEGU3KRFEdT5rzBHhuwa5yGECc9ioINVEGFkbC5hZGZpbHRlci5uZXQKL2Rucy1xdWVyeQ"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": false, "addr": "163.47.117.176", "hashs": ["cc1060d39c8329b62b6fbc7d0d6df9309869b981e7e6392d5cd8fa408f4d80e6", "f7f5af28801e877d7df3a2be28fe149f3274a7ed29f136fd503ce398cba80b0d", "b35e05950cfbd8259f9375ba101870b8c3c4c0ec5a529757e63167e9de26a9e8", "444ebd67bb83f8807b3921e938ac9178b882bd50aadb11231f044cf5f08df7ce", "e644ba6963e335fe765cb9976b12b10eb54294b42477764ccb3a3acca3acb2fc", "2aae3fb7bf05e4c81c4194dca44511d4f9af304786ec1ae7218409cf62a08355"], "hostname": "adl.adfilter.net", "path": "/dns-query"}'
+        comman_parse_test(self, stamp, DNSoverHTTPS, except_result_json)
+
+    def test_DNSoverTLS(self):
+        stamp = "sdns://AwcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": true, "addr": "1.1.1.1", "hashs": [], "hostname": "one.one.one.one"}'
+        comman_parse_test(self, stamp, DNSoverTLS, except_result_json)
+
+    def test_DNSoverQUIC(self):
+        stamp = "sdns://BAcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": true, "addr": "1.1.1.1", "hashs": [], "hostname": "one.one.one.one"}'
+        comman_parse_test(self, stamp, DNSoverQUIC, except_result_json)
+
+    def test_ObliviousDoH(self):
+        stamp = "sdns://BQcAAAAAAAAADWpwLnRpYXJhcC5vcmcFL29kb2g"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": true, "hostname": "jp.tiarap.org", "path": "/odoh"}'
+        comman_parse_test(self, stamp, ObliviousDoH, except_result_json)
+
+    def test_DNSCryptRelay(self):
+        stamp = "sdns://gQ04Ni4xMDYuNzQuMjE5"
+        except_result_json = '{"addr": "86.106.74.219"}'
+        comman_parse_test(self, stamp, DNSCryptRelay, except_result_json)
+
+    def test_ObliviousDoHRelay(self):
+        stamp = "sdns://hQcAAAAAAAAADDg5LjM4LjEzMS4zOAAYb2RvaC1ubC5hbGVrYmVyZy5uZXQ6NDQzBi9wcm94eQ"
+        except_result_json = '{"dnssec": true, "nolog": true, "nofilter": true, "addr": "89.38.131.38", "hashs": [], "hostname": "odoh-nl.alekberg.net:443", "path": "/proxy"}'
+        comman_parse_test(self, stamp, ObliviousDoHRelay, except_result_json)
+
+    def test_PlainDNS(self):
+        stamp = "sdns://AAUAAAAAAAAABzEuMS4xLjE"
+        except_result_json = '{"dnssec": true, "nolog": false, "nofilter": true, "addr": "1.1.1.1"}'
+        comman_parse_test(self, stamp, PlainDNS, except_result_json)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/test_dnscrypt_to_smartdns.py b/testing/test_dnscrypt_to_smartdns.py
new file mode 100644
index 0000000..3b21a18
--- /dev/null
+++ b/testing/test_dnscrypt_to_smartdns.py
@@ -0,0 +1,14 @@
+import unittest
+
+from dnscrypt_to_smartdns import get_smartdns_config
+
+
+class MyTestCase(unittest.TestCase):
+    def test_get_smartdns_config(self):
+        conf_text = get_smartdns_config()
+        # print(conf_text)
+        self.assertIsInstance(conf_text, str)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/test_gfwlist_to_dns.py b/testing/test_gfwlist_to_dns.py
new file mode 100644
index 0000000..8394996
--- /dev/null
+++ b/testing/test_gfwlist_to_dns.py
@@ -0,0 +1,19 @@
+import unittest
+
+from gfwlist_to_dns import get_dnsmasq_text, get_smartdns_domain_set
+
+
+class MyTestCase(unittest.TestCase):
+    def test_get_dnsmasq_text(self):
+        dnsmasq_text = get_dnsmasq_text()
+        # print(dnsmasq_text)
+        self.assertIsInstance(dnsmasq_text, str)
+
+    def test_get_smartdns_domain_set(self):
+        domain_set = get_smartdns_domain_set()
+        # print(domain_set)
+        self.assertIsInstance(domain_set, str)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/utils/dnsstamps.py b/utils/dnsstamps.py
deleted file mode 100644
index 12311e8..0000000
--- a/utils/dnsstamps.py
+++ /dev/null
@@ -1,537 +0,0 @@
-import base64
-
-
-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)
-
-
-# Code from https://github.com/DNSCrypt/dnscrypt-resolvers/blob/21fcbaf858112c63fed2a504714cc829bd654483/utils/format.py#L101-L141
-class DNSCrypt:
-    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()
-        i = i + 1
-
-        parsed = DNSCrypt()
-
-        # props
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr [:port])
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        # LP(pk)
-        pk_len = b[i]
-        i = i + 1
-        if pk_len != 32:
-            raise ValueError()
-        hpk = b[i:i + pk_len].hex().upper()
-        hpks = []
-        for j in range(0, 16):
-            hpks.append(hpk[j * 4: j * 4 + 4])
-        parsed.pk = ":".join(hpks)
-        i = i + pk_len
-
-        # LP(providerName)
-        provider_len = b[i]
-        i = i + 1
-        parsed.provider = b[i:i + provider_len].decode("utf-8")
-        i = i + provider_len
-
-        return parsed
-
-
-class DNSoverHTTPS:
-    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()
-        i = i + 1
-
-        parsed = DNSoverHTTPS()
-
-        # props
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        # VLP(hash1, hash2, ...hashn)
-        last_element = False
-        while True:
-            if b[i] & 0x80 == 0:
-                last_element = True
-                hashx_len = b[i]
-            else:
-                hashx_len = b[i] ^ 0x80
-            if hashx_len != 0 and hashx_len != 32:
-                raise ValueError()
-            i = i + 1
-            if hashx_len > 0:
-                hashx = b[i:i + hashx_len].hex()
-                parsed.hashs.append(hashx)
-                i = i + hashx_len
-            if last_element:
-                break
-
-        # LP(hostname [:port])
-        hostname_len = b[i]
-        i = i + 1
-        parsed.hostname = b[i:i + hostname_len].decode("utf-8")
-        i = i + hostname_len
-
-        # LP(path)
-        path_len = b[i]
-        i = i + 1
-        parsed.path = b[i:i + path_len].decode("utf-8")
-        i = i + path_len
-
-        # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
-        if i < len(b):
-            last_element = False
-            while True:
-                if b[i] & 0x80 == 0:
-                    last_element = True
-                    bootstrap_ipx_len = b[i]
-                else:
-                    bootstrap_ipx_len = b[i] ^ 0x80
-                i = i + 1
-                if bootstrap_ipx_len > 0:
-                    bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8")
-                    parsed.bootstrap_ipi.append(bootstrap_ipx)
-                    i = i + bootstrap_ipx_len
-                if last_element:
-                    break
-
-        return parsed
-
-
-class DNSoverTLS:
-    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
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        # VLP(hash1, hash2, ...hashn)
-        last_element = False
-        while True:
-            if b[i] & 0x80 == 0:
-                last_element = True
-                hashx_len = b[i]
-            else:
-                hashx_len = b[i] ^ 0x80
-            if hashx_len != 0 and hashx_len != 32:
-                raise ValueError()
-            i = i + 1
-            if hashx_len > 0:
-                hashx = b[i:i + hashx_len].hex()
-                parsed.hashs.append(hashx)
-                i = i + hashx_len
-            if last_element:
-                break
-
-        # LP(hostname[:port])
-        hostname_len = b[i]
-        i = i + 1
-        parsed.hostname = b[i:i + hostname_len].decode("utf-8")
-        i = i + hostname_len
-
-        # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
-        if i < len(b):
-            last_element = False
-            while True:
-                if b[i] & 0x80 == 0:
-                    last_element = True
-                    bootstrap_ipx_len = b[i]
-                else:
-                    bootstrap_ipx_len = b[i] ^ 0x80
-                i = i + 1
-                if bootstrap_ipx_len > 0:
-                    bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8")
-                    parsed.bootstrap_ipi.append(bootstrap_ipx)
-                    i = i + bootstrap_ipx_len
-                if last_element:
-                    break
-
-        return parsed
-
-
-class DNSoverQUIC:
-    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
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        # VLP(hash1, hash2, ...hashn)
-        last_element = False
-        while True:
-            if b[i] & 0x80 == 0:
-                last_element = True
-                hashx_len = b[i]
-            else:
-                hashx_len = b[i] ^ 0x80
-            if hashx_len != 0 and hashx_len != 32:
-                raise ValueError()
-            i = i + 1
-            if hashx_len > 0:
-                hashx = b[i:i + hashx_len].hex()
-                parsed.hashs.append(hashx)
-                i = i + hashx_len
-            if last_element:
-                break
-
-        # LP(hostname[:port])
-        hostname_len = b[i]
-        i = i + 1
-        parsed.hostname = b[i:i + hostname_len].decode("utf-8")
-        i = i + hostname_len
-
-        # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
-        if i < len(b):
-            last_element = False
-            while True:
-                if b[i] & 0x80 == 0:
-                    last_element = True
-                    bootstrap_ipx_len = b[i]
-                else:
-                    bootstrap_ipx_len = b[i] ^ 0x80
-                i = i + 1
-                if bootstrap_ipx_len > 0:
-                    bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8")
-                    parsed.bootstrap_ipi.append(bootstrap_ipx)
-                    i = i + bootstrap_ipx_len
-                if last_element:
-                    break
-
-        return parsed
-
-
-class ObliviousDoH:
-    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
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(hostname [:port])
-        hostname_len = b[i]
-        i = i + 1
-        parsed.hostname = b[i:i + hostname_len].decode("utf-8")
-        i = i + hostname_len
-
-        # LP(path)
-        path_len = b[i]
-        i = i + 1
-        parsed.path = b[i:i + path_len].decode("utf-8")
-        i = i + path_len
-
-        return parsed
-
-
-class DNSCryptRelay:
-    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)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        return parsed
-
-
-class ObliviousDoHrelay:
-    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
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        # VLP(hash1, hash2, ...hashn)
-        last_element = False
-        while True:
-            if b[i] & 0x80 == 0:
-                last_element = True
-                hashx_len = b[i]
-            else:
-                hashx_len = b[i] ^ 0x80
-            if hashx_len != 0 and hashx_len != 32:
-                raise ValueError()
-            i = i + 1
-            if hashx_len > 0:
-                hashx = b[i:i + hashx_len].hex()
-                parsed.hashs.append(hashx)
-                i = i + hashx_len
-            if last_element:
-                break
-
-        # LP(hostname [:port])
-        hostname_len = b[i]
-        i = i + 1
-        parsed.hostname = b[i:i + hostname_len].decode("utf-8")
-        i = i + hostname_len
-
-        # LP(path)
-        path_len = b[i]
-        i = i + 1
-        parsed.path = b[i:i + path_len].decode("utf-8")
-        i = i + path_len
-
-        # VLP(bootstrap_ip1, bootstrap_ip2, ...bootstrap_ipn) (optional)
-        if i < len(b):
-            last_element = False
-            while True:
-                if b[i] & 0x80 == 0:
-                    last_element = True
-                    bootstrap_ipx_len = b[i]
-                else:
-                    bootstrap_ipx_len = b[i] ^ 0x80
-                i = i + 1
-                if bootstrap_ipx_len > 0:
-                    bootstrap_ipx = b[i:i + bootstrap_ipx_len].decode("utf-8")
-                    parsed.bootstrap_ipi.append(bootstrap_ipx)
-                    i = i + bootstrap_ipx_len
-                if last_element:
-                    break
-
-        return parsed
-
-
-class PlainDNS:
-    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
-        props = b[i]
-        parsed.dnssec = not not ((props >> 0) & 1)
-        parsed.nolog = not not ((props >> 1) & 1)
-        parsed.nofilter = not not ((props >> 2) & 1)
-        i = i + 8
-
-        # LP(addr)
-        addr_len = b[i]
-        i = i + 1
-        parsed.addr = b[i:i + addr_len].decode("utf-8")
-        i = i + addr_len
-
-        return parsed
-
-
-if __name__ == '__main__':
-    # DNSCrypt
-    t = parse(
-        "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ")
-    print(t)
-
-    # DoH
-    t = parse("sdns://AgcAAAAAAAAADTIxNy4xNjkuMjAuMjIADWRucy5hYS5uZXQudWsKL2Rucy1xdWVyeQ")
-    t = parse(
-        "sdns://AgMAAAAAAAAADjE2My40Ny4xMTcuMTc2oMwQYNOcgym2K2-8fQ1t-TCYabmB5-Y5LVzY-kCPTYDmoPf1ryiAHod9ffOivij-FJ8ydKftKfE2_VA845jLqAsNoLNeBZUM-9gln5N1uhAYcLjDxMDsWlKXV-YxZ-neJqnooEROvWe7g_iAezkh6TiskXi4gr1QqtsRIx8ETPXwjffOoOZEumlj4zX-dly5l2sSsQ61QpS0JHd2TMs6OsyjrLL8ICquP7e_BeTIHEGU3KRFEdT5rzBHhuwa5yGECc9ioINVEGFkbC5hZGZpbHRlci5uZXQKL2Rucy1xdWVyeQ")
-    print(t)
-
-    # DoT
-    t = parse("sdns://AwcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ")
-    print(t)
-
-    # DoQ
-    t = parse("sdns://BAcAAAAAAAAABzEuMS4xLjEAD29uZS5vbmUub25lLm9uZQ")
-    print(t)
-
-    # oDoH
-    t = parse("sdns://BQcAAAAAAAAADWpwLnRpYXJhcC5vcmcFL29kb2g")
-    print(t)
-
-    #  DNSCrypt relay
-    t = parse("sdns://gQ04Ni4xMDYuNzQuMjE5")
-    print(t)
-
-    #  oDoH relay
-    t = parse("sdns://hQcAAAAAAAAADDg5LjM4LjEzMS4zOAAYb2RvaC1ubC5hbGVrYmVyZy5uZXQ6NDQzBi9wcm94eQ")
-    print(t)
-
-    # Plain DNS
-    t = parse("sdns://AAUAAAAAAAAABzEuMS4xLjE")
-    print(t)