From e967abd42f14160f21f67ca5e6ac99b0be4d2f9e Mon Sep 17 00:00:00 2001 From: Moraxyc Date: Sat, 4 Jul 2026 12:58:38 +0800 Subject: [PATCH 1/2] unbound: add DNS-over-QUIC support --- pkgs/by-name/un/unbound/package.nix | 12 +++++++++++- pkgs/top-level/all-packages.nix | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkgs/by-name/un/unbound/package.nix b/pkgs/by-name/un/unbound/package.nix index 935818521103..b2b52eadbb27 100644 --- a/pkgs/by-name/un/unbound/package.nix +++ b/pkgs/by-name/un/unbound/package.nix @@ -31,6 +31,8 @@ systemd ? null, # optionally support DNS-over-HTTPS as a server withDoH ? false, + # optionally support DNS-over-QUIC as a server + withDoQ ? false, withECS ? false, withDNSCrypt ? false, withDNSTAP ? false, @@ -38,7 +40,7 @@ withRedis ? false, # Avoid .lib depending on lib.getLib openssl # The build gets a little hacky, so in some cases we disable this approach. - withSlimLib ? stdenv.hostPlatform.isLinux && !stdenv.hostPlatform.isMusl && !withDNSTAP, + withSlimLib ? stdenv.hostPlatform.isLinux && !stdenv.hostPlatform.isMusl && !withDNSTAP && !withDoQ, # enable support for python plugins in unbound: note this is distinct from pyunbound # see https://unbound.docs.nlnetlabs.nl/en/latest/developer/python-modules.html withPythonModule ? false, @@ -47,6 +49,7 @@ withLto ? !stdenv.hostPlatform.isStatic && !stdenv.hostPlatform.isMinGW, withMakeWrapper ? !stdenv.hostPlatform.isMinGW, libnghttp2, + ngtcp2, # for passthru.updateScript nix-update-script, @@ -55,6 +58,9 @@ versionCheckHook, }: +assert lib.assertMsg ( + !withDoQ || lib.versionAtLeast openssl.version "3.5.0" +) "unbound: withDoQ requires OpenSSL with QUIC support (OpenSSL >= 3.5)"; stdenv.mkDerivation (finalAttrs: { pname = "unbound"; version = "1.25.1"; @@ -90,6 +96,7 @@ stdenv.mkDerivation (finalAttrs: { ] ++ lib.optionals withSystemd [ systemd ] ++ lib.optionals withDoH [ libnghttp2 ] + ++ lib.optionals withDoQ [ ngtcp2 ] ++ lib.optionals withPythonModule [ python ]; enableParallelBuilding = true; @@ -120,6 +127,9 @@ stdenv.mkDerivation (finalAttrs: { ++ lib.optionals withDoH [ "--with-libnghttp2=${libnghttp2.dev}" ] + ++ lib.optionals withDoQ [ + "--with-libngtcp2=${ngtcp2.dev}" + ] ++ lib.optionals withECS [ "--enable-subnet" ] diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index f4e48cfb7c1c..a422a0b618c9 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -3054,6 +3054,7 @@ with pkgs; withDynlibModule = true; withPythonModule = true; withDoH = true; + withDoQ = true; withECS = true; withDNSCrypt = true; withDNSTAP = true; From ed374affd1167188e97835e643b5a18fe37ea5fe Mon Sep 17 00:00:00 2001 From: Moraxyc Date: Sat, 4 Jul 2026 12:59:03 +0800 Subject: [PATCH 2/2] nixosTests.unbound: support doq --- nixos/tests/unbound.nix | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/nixos/tests/unbound.nix b/nixos/tests/unbound.nix index 1e656fc5fcf2..795f4c11c74f 100644 --- a/nixos/tests/unbound.nix +++ b/nixos/tests/unbound.nix @@ -7,6 +7,7 @@ * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via TCP/853 (DoT) * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/53 & UDP/53 * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/853 (DoT) + * running a recursive DNS resolver on a machine in the network awaiting input from clients over UDP/853 (DoQ) In the below test setup we are trying to implement all of those use cases. @@ -100,7 +101,7 @@ in }; # The resolver that knows that forwards (only) to the authoritative server - # and listens on UDP/53, TCP/53 & TCP/853. + # and listens on UDP/53, TCP/53, TCP/853 & UDP/853. resolver = { lib, nodes, ... }: { @@ -122,7 +123,10 @@ in 853 # DNS over TLS 443 # DNS over HTTPS ]; - networking.firewall.allowedUDPPorts = [ 53 ]; + networking.firewall.allowedUDPPorts = [ + 53 # regular DNS + 853 # DNS over QUIC + ]; services.unbound = { enable = true; @@ -150,6 +154,8 @@ in ]; tls-service-pem = "${cert}/cert.pem"; tls-service-key = "${cert}/key.pem"; + quic-port = 853; + quic-size = "8m"; }; forward-zone = [ { @@ -306,7 +312,7 @@ in assert expected == out, f"Expected `{expected}` but got `{out}`" - def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]): + def test(machine, remotes, /, doh=False, doq=False, zone=zone, records=records, args=[]): """ Run queries for the given remotes on the given machine. """ @@ -331,6 +337,15 @@ in expected, ["+https"] + args, ) + if doq: + query( + machine, + remote, + query_type, + zone, + expected, + ["+quic"] + args, + ) client.start() @@ -348,12 +363,12 @@ in # verify that the resolver is able to resolve on all the local protocols with subtest("test that the resolver resolves on all protocols and transports"): - test(resolver, ["::1", "127.0.0.1"], doh=True) + test(resolver, ["::1", "127.0.0.1"], doh=True, doq=True) resolver.wait_for_unit("multi-user.target") with subtest("client should be able to query the resolver"): - test(client, ["${(lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address}", "${(lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address}"], doh=True) + test(client, ["${(lib.head nodes.resolver.networking.interfaces.eth1.ipv6.addresses).address}", "${(lib.head nodes.resolver.networking.interfaces.eth1.ipv4.addresses).address}"], doh=True, doq=True) # discard the client we do not need anymore client.shutdown()