Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ RUN mkdir -p \
COPY --chmod=644 encrypted-dns.toml.in /opt/encrypted-dns/etc/
COPY --chmod=644 undelegated.txt /opt/encrypted-dns/etc/

COPY --chmod=644 opennic.hints /opt/unbound/etc/unbound/
COPY --chmod=644 opennic.key /opt/unbound/etc/unbound/

COPY --chmod=755 entrypoint.sh /

COPY --chmod=755 unbound.sh /var/svc/unbound/run
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Table of contents:
- [Prometheus metrics](#prometheus-metrics)
- [TLS (including HTTPS and DoH) forwarding](#tls-including-https-and-doh-forwarding)
- [Filtering](#filtering)
- [OpenNIC](#opennic)
- [Join the network](#join-the-network)
- [Usage with Docker Compose](#usage-with-docker-compose)
- [Usage with Kubernetes](#usage-with-kubernetes)
Expand Down Expand Up @@ -186,6 +187,35 @@ And put the list of domains to block in a file named `/etc/dnscrypt-server/lists

Then, follow the upgrade procedure, adding the following option to the `docker run` command: `-v /etc/dnscrypt-server/lists:/opt/encrypted-dns/etc/lists`.

## OpenNIC

The server can resolve names using the [OpenNIC](https://www.opennic.org/)
root instead of the ICANN root, giving access to the OpenNIC TLDs (`.bbs`,
`.chan`, `.cyb`, `.dyn`, `.epic`, `.geek`, `.gopher`, `.indy`, `.libre`,
`.neo`, `.null`, `.o`, `.oss`, `.oz`, `.parody`, `.pirate`) in addition to
the regular namespace.

Enable it by setting the `OPENNIC=1` environment variable when creating the
container (add `-e OPENNIC=1` to the `docker run` command). Without it, the
image keeps the default behavior and resolves from the ICANN root.

In OpenNIC mode:

- Unbound bootstraps from the OpenNIC root servers (`opennic.hints`) and never
contacts the ICANN root servers, internic.net, or IANA.
- DNSSEC validation is anchored to the OpenNIC root key (`opennic.key`) and
kept up to date automatically (RFC 5011). The OpenNIC root also serves the
DS records of the ICANN TLDs, so DNSSEC keeps working for the regular
namespace.

If the OpenNIC root server set or root key ever changes, refresh both files
and rebuild the image:

```sh
dig +https @dns1.slowb.ro . NS # compare against opennic.hints
dig +https @dns1.slowb.ro . DNSKEY # compare the KSK (flags 257) against opennic.key
```

# Join the network

If you want to help against DNS centralization and surveillance,
Expand Down Expand Up @@ -295,6 +325,8 @@ docker rmi --force jedisct1/dnscrypt-server ||:
- Caching resolver: [Unbound](https://www.unbound.net/), with DNSSEC, prefetching,
and no logs. The number of threads and memory usage are automatically adjusted.
Latest stable version, compiled from source. qname minimisation is enabled.
Optionally resolves from the [OpenNIC](https://www.opennic.org/) root (see
[OpenNIC](#opennic)).
- [encrypted-dns-server](https://github.com/jedisct1/encrypted-dns-server).
Compiled from source.

Expand Down
4 changes: 4 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ start() {
ln -s -f "$service" "${SERVICES_DIR}/"
done

# runit wipes the environment before running services, so persist
# settings that service scripts need to a file.
printf '%s\n' "${OPENNIC:-}" >/etc/opennic-env

exec /etc/runit/2 </dev/null >/dev/null 2>/dev/null
}

Expand Down
18 changes: 18 additions & 0 deletions opennic.hints
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
; OpenNIC root hints
; https://servers.opennicproject.org/
; Verified 2026-07-14 against two independent OpenNIC DoH servers.
; Refresh with: dig +https @dns1.slowb.ro . NS (then A/AAAA for each NS)
. 3600000 NS ns2.opennic.glue.
. 3600000 NS ns4.opennic.glue.
. 3600000 NS ns6.opennic.glue.
. 3600000 NS ns7.opennic.glue.
. 3600000 NS ns8.opennic.glue.
ns2.opennic.glue. 3600000 A 161.97.219.84
ns2.opennic.glue. 3600000 AAAA 2001:470:4212:10:0:100:53:10
ns4.opennic.glue. 3600000 A 116.203.104.203
ns4.opennic.glue. 3600000 AAAA 2a01:4f8:c2c:da9c::1
ns6.opennic.glue. 3600000 A 45.79.189.158
ns7.opennic.glue. 3600000 A 153.56.130.67
ns7.opennic.glue. 3600000 AAAA 2001:470:7493::e0
ns8.opennic.glue. 3600000 A 178.63.116.152
ns8.opennic.glue. 3600000 AAAA 2a01:4f8:141:4281::999
5 changes: 5 additions & 0 deletions opennic.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
; OpenNIC root DNSSEC trust anchor (KSK, key tag 60820, alg 8/RSASHA256)
; Corresponding DS: 60820 8 2 A01E33C8E95712E555FA9E6C09921830F3A518E36C5998F4ADBF5570AA86B538
; Verified 2026-07-14 against two OpenNIC DoH servers and https://wiki.opennic.org/opennic/dnssec
; Seeds var/opennic.key on first start; unbound then maintains it via RFC 5011.
. 86400 IN DNSKEY 257 3 8 AwEAAbtbsu+wl3fbEDbgvMgJ1BDXeAk5t6BU7B1KGVvc13zMJtjvarxpWWrAb7fmWERX8kJawa3KpYty0EDFQ24nfQyhwEOld442ca89u4/ZU3jPuwKohbGn55vIQ7KjCIrDNvRYjGVn2MNwZnL4WVVclJYsa1cGwVQ9t575I5yvU+5g+jVcjUsGwFn6xmuJC0Z33ABKsC8b1cjfcnvE4wP3CrXOlDQ+Er4uPUtMKrmG+Sj1Bm5U+do78mwEXOlTz/sNj8tkpL0pYB2j+XNaDVrO0uS1beekejnttMsC4SHMCsiwMvigW2O54ByhzijU2v87d7U9WEMVfPvO6gearg1fo/1Tk4buzPZcS+W9WZgFAt7kT1ois3x0GGT7J55zENB9IZU4tMmWdYbZJOZsdAzmshuWJIUlTZdNN5671Rhc6P9TWnMlvb9iNT7G3DZ9PhBw1OF/OmmXobv3Wygbt5+u7q2CPPzwU4WTGpVNtr3Iry2SPW3XVpJSM3+nW7LfxxtWZJlN4MDQYC5IptU+A5EO80/yE38E9tKGDWC1+Nw59QLaBE7ff+Jkq7OMjTHjFhYivkJSv+8LEbkGjWoaMAS2CT3/ZVMYLiQn8THiZUBF+aOzJMw0EGPag1Qq4vfGgFkQMM3hOaH6bWN1yCvmspuiwLYkNCZZ/l8ThKc57bGYy9TX
35 changes: 33 additions & 2 deletions unbound.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ else
threads=1
fi

if [ -z "${OPENNIC:-}" ] && [ -f /etc/opennic-env ]; then
OPENNIC=$(cat /etc/opennic-env)
fi
opennic_enabled=0
case "${OPENNIC:-}" in
1 | true | yes | on) opennic_enabled=1 ;;
esac

provider_name=$(cat "$KEYS_DIR/provider_name")

sed \
Expand Down Expand Up @@ -57,7 +65,6 @@ server:
udp-connect: no
chroot: "/opt/unbound/etc/unbound"
directory: "/opt/unbound/etc/unbound"
auto-trust-anchor-file: "var/root.key"
num-queries-per-thread: 4096
outgoing-range: 8192
msg-cache-size: @MSG_CACHE_SIZE@
Expand Down Expand Up @@ -130,6 +137,21 @@ server:
remote-control:
control-enable: yes
control-interface: 127.0.0.1
EOT

if [ "$opennic_enabled" = 1 ]; then
cat >>/opt/unbound/etc/unbound/unbound.conf <<EOT

server:
# OpenNIC root: no ICANN root servers, internic.net, or IANA involved
root-hints: "opennic.hints"
auto-trust-anchor-file: "var/opennic.key"
EOT
else
cat >>/opt/unbound/etc/unbound/unbound.conf <<EOT

server:
auto-trust-anchor-file: "var/root.key"

auth-zone:
name: "."
Expand All @@ -139,13 +161,22 @@ auth-zone:
for-upstream: yes
zonefile: "var/root.zone"
EOT
fi

mkdir -p /opt/unbound/etc/unbound/dev &&
cp -a /dev/random /dev/urandom /opt/unbound/etc/unbound/dev/

mkdir -p -m 700 /opt/unbound/etc/unbound/var &&
chown _unbound:_unbound /opt/unbound/etc/unbound/var &&
chown _unbound:_unbound /opt/unbound/etc/unbound/var

if [ "$opennic_enabled" = 1 ]; then
if [ ! -f /opt/unbound/etc/unbound/var/opennic.key ]; then
cp /opt/unbound/etc/unbound/opennic.key /opt/unbound/etc/unbound/var/opennic.key
fi
chown _unbound:_unbound /opt/unbound/etc/unbound/var/opennic.key
else
/opt/unbound/sbin/unbound-anchor -a /opt/unbound/etc/unbound/var/root.key
fi

if [ ! -f /opt/unbound/etc/unbound/unbound_control.pem ]; then
/opt/unbound/sbin/unbound-control-setup 2>/dev/null || :
Expand Down
Loading