]> git.ekhem.eu.org Git - guix.git/commitdiff
[vps] Add dnscrypt-proxy.
authorJakub Czajka <jakub@ekhem.eu.org>
Wed, 15 Jul 2026 22:52:27 +0000 (22:52 +0000)
committerJakub Czajka <jakub@ekhem.eu.org>
Wed, 15 Jul 2026 22:52:27 +0000 (22:52 +0000)
Package dnscrypt-proxy 2.1.7 with Shepherd service and
resolv-conf-service-type that extends etc-service-type.
Wire into vps-operating-system (dedicated user/group,
resolver pointing at 127.0.0.1).  Test dnscrypt-proxy
service, port 53 TCP, resolv.conf, and NTP daemon.

Co-Authored-By: Claude <noreply@anthropic.com>
conf/vps/dnscrypt.scm [new file with mode: 0644]
dnscrypt-proxy/etc/blocked-names.txt [new file with mode: 0644]
dnscrypt-proxy/etc/dnscrypt-proxy.toml [new file with mode: 0644]
tests/dnscrypt.scm [new file with mode: 0644]
tests/ntp.scm [new file with mode: 0644]
tests/vps-base.scm
vps-system.scm

diff --git a/conf/vps/dnscrypt.scm b/conf/vps/dnscrypt.scm
new file mode 100644 (file)
index 0000000..43c081b
--- /dev/null
@@ -0,0 +1,121 @@
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; dnscrypt.scm — dnscrypt-proxy 2.x DNS proxy for the VPS.
+
+(define-module (conf vps dnscrypt)
+  #:use-module (guix packages)
+  #:use-module (guix download)
+  #:use-module (guix gexp)
+  #:use-module (guix build-system trivial)
+  #:use-module ((guix licenses)
+                #:prefix license:)
+  #:use-module (gnu packages base)
+  #:use-module (gnu packages compression)
+  #:use-module (gnu services)
+  #:use-module (gnu services shepherd)
+  #:export (dnscrypt-proxy dnscrypt-proxy-shepherd-service
+                           %dnscrypt-resolv-conf resolv-conf-service-type))
+
+(define toml-config
+  (local-file (canonicalize-path (string-append (getenv "GUIX_PACKAGE_PATH")
+                                  "/dnscrypt-proxy/etc/dnscrypt-proxy.toml"))
+              "dnscrypt-proxy.toml"))
+
+(define blocked-names
+  (local-file (canonicalize-path (string-append (getenv "GUIX_PACKAGE_PATH")
+                                  "/dnscrypt-proxy/etc/blocked-names.txt"))
+              "blocked-names.txt"))
+
+(define dnscrypt-proxy
+  (package
+    (name "dnscrypt-proxy")
+    (version "2.1.7")
+    (source
+     (origin
+       (method url-fetch)
+       (uri (string-append "https://github.com/DNSCrypt/dnscrypt-proxy/"
+                           "releases/download/"
+                           version
+                           "/dnscrypt-proxy-linux_x86_64-"
+                           version
+                           ".tar.gz"))
+       (sha256
+        (base32 "1sgc3yc6zg5fzmi23g0aa6sr123mpir7jj3r42fspyngx0dbmfi5"))))
+    (build-system trivial-build-system)
+    (arguments
+     (list
+      #:modules '((guix build utils))
+      #:builder
+      #~(begin
+          (use-modules (guix build utils))
+          (let* ((out #$output)
+                 (sbin (string-append out "/sbin"))
+                 (etc (string-append out "/etc"))
+                 (tmp "/tmp/dnscrypt-proxy-build"))
+            ;; Unpack the release tarball.
+            (mkdir-p tmp)
+            (setenv "PATH"
+                    (string-append #$(file-append gzip "/bin")))
+            (invoke #$(file-append tar "/bin/tar")
+                    "xzf"
+                    #$source
+                    "-C"
+                    tmp
+                    "--strip-components=1")
+            ;; Binary -- Go, statically linked, no patchelf needed.
+            (mkdir-p sbin)
+            (copy-file (string-append tmp "/dnscrypt-proxy")
+                       (string-append sbin "/dnscrypt-proxy"))
+            (chmod (string-append sbin "/dnscrypt-proxy") #o755)
+            ;; Config files.
+            (mkdir-p etc)
+            (copy-file #$toml-config
+                       (string-append etc "/dnscrypt-proxy.toml"))
+            (copy-file #$blocked-names
+                       (string-append etc "/blocked-names.txt"))
+            ;; Point the TOML at the blocked-names.txt in the store,
+            ;; replacing the human-readable /etc/dns/ placeholder.
+            (substitute* (string-append etc "/dnscrypt-proxy.toml")
+              (("'/etc/dns/blocked-names\\.txt'")
+               (string-append "'" etc "/blocked-names.txt'")))))))
+    (inputs (list tar gzip))
+    (home-page "https://github.com/DNSCrypt/dnscrypt-proxy")
+    (synopsis "Encrypted DNS proxy (v2)")
+    (description
+     "dnscrypt-proxy encrypts and authenticates DNS traffic between
+your machine and upstream resolvers that support the DNSCrypt or DoH
+protocols.  This is version 2.x, the Go rewrite with built-in caching,
+domain blocking and schedule support.")
+    (license license:isc)))
+
+(define dnscrypt-proxy-shepherd-service
+  (let ((bin (file-append dnscrypt-proxy "/sbin/dnscrypt-proxy"))
+        (cfg (file-append dnscrypt-proxy "/etc/dnscrypt-proxy.toml")))
+    (shepherd-service (provision '(dnscrypt-proxy))
+                      (requirement '(user-processes networking))
+                      (respawn? #t)
+                      ;; Binary drops privileges via user_name in the TOML.
+                      ;; Do not pass #:user -- it must start as root.
+                      (start #~(make-forkexec-constructor (list #$bin
+                                                                "-config"
+                                                                #$cfg)
+                                #:log-file "/var/log/dnscrypt-proxy.log"))
+                      (stop #~(make-kill-destructor))
+                      (documentation "dnscrypt-proxy encrypted DNS proxy."))))
+
+(define %dnscrypt-resolv-conf
+  (plain-file "resolv.conf" "nameserver 127.0.0.1\noptions edns0\n"))
+
+;; Extends etc-service-type to install the resolv.conf.  Uses its
+;; own service type rather than simple-service to avoid creating
+;; a second etc-service-type instance (which fold-services would
+;; reject).
+(define resolv-conf-service-type
+  (let ((etc-entry (list (list "resolv.conf" %dnscrypt-resolv-conf))))
+    (service-type (name 'resolv-conf)
+                  (extensions (list (service-extension etc-service-type
+                                                       (const etc-entry))))
+                  (default-value #f)
+                  (description
+                   "Point /etc/resolv.conf to dnscrypt-proxy on localhost."))))
diff --git a/dnscrypt-proxy/etc/blocked-names.txt b/dnscrypt-proxy/etc/blocked-names.txt
new file mode 100644 (file)
index 0000000..817d98d
--- /dev/null
@@ -0,0 +1,5 @@
+# Blocked domain names for dnscrypt-proxy.
+# Lines starting with # are comments.
+# Add one domain per line.  OISD is a good source:
+#   https://oisd.nl/
+
diff --git a/dnscrypt-proxy/etc/dnscrypt-proxy.toml b/dnscrypt-proxy/etc/dnscrypt-proxy.toml
new file mode 100644 (file)
index 0000000..ea78085
--- /dev/null
@@ -0,0 +1,724 @@
+##############################################
+#                                            #
+#        dnscrypt-proxy configuration        #
+#                                            #
+##############################################
+
+## This is an example configuration file.
+## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml"
+##
+## Online documentation is available here: https://dnscrypt.info/doc
+
+
+
+##################################
+#         Global settings        #
+##################################
+
+## List of servers to use
+##
+## Servers from the "public-resolvers" source (see down below) can
+## be viewed here: https://dnscrypt.info/public-servers
+##
+## The proxy will automatically pick working servers from this list.
+## Note that the require_* filters do NOT apply when using this setting.
+##
+## By default, this list is empty and all registered servers matching the
+## require_* filters will be used instead.
+##
+## Remove the leading # first to enable this; lines starting with # are ignored.
+
+# server_names = ['scaleway-fr', 'google', 'yandex', 'cloudflare']
+
+
+## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6.
+## Example with both IPv4 and IPv6:
+## listen_addresses = ['127.0.0.1:53', '[::1]:53']
+##
+## To listen to all IPv4 addresses, use `listen_addresses = ['0.0.0.0:53']`
+## To listen to all IPv4+IPv6 addresses, use `listen_addresses = ['[::]:53']`
+
+listen_addresses = ['0.0.0.0:53']
+
+
+## Maximum number of simultaneous client connections to accept
+
+max_clients = 250
+
+
+## Switch to a different system user after listening sockets have been created.
+## Note (1): this feature is currently unsupported on Windows.
+## Note (2): this feature is not compatible with systemd socket activation.
+## Note (3): when using -pidfile, the PID file directory must be writable by the new user
+
+user_name = 'dnscrypt-proxy'
+
+
+## Require servers (from remote sources) to satisfy specific properties
+
+# Use servers reachable over IPv4
+ipv4_servers = true
+
+# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity
+ipv6_servers = false
+
+# Use servers implementing the DNSCrypt protocol
+dnscrypt_servers = true
+
+# Use servers implementing the DNS-over-HTTPS protocol
+doh_servers = true
+
+# Use servers implementing the Oblivious DoH protocol
+odoh_servers = false
+
+
+## Require servers defined by remote sources to satisfy specific properties
+
+# Server must support DNS security extensions (DNSSEC)
+require_dnssec = true
+
+# Server must not log user queries (declarative)
+require_nolog = true
+
+# Server must not enforce its own blocklist (for parental control, ads blocking...)
+require_nofilter = true
+
+## Require servers (from remote sources) to satisfy specific properties
+
+server_names = ['dnscrypt.eu-nl', 'dnscrypt.uk-ipv4', 'ffmuc.net', 'meganerd', 'publicarray-au-doh', 'scaleway-ams', 'scaleway-fr', 'v.dnscrypt.uk-ipv4']
+
+# Server names to avoid even if they match all criteria
+disabled_server_names = []
+
+
+## Always use TCP to connect to upstream servers.
+## This can be useful if you need to route everything through a SOCKS v5
+## proxy, or if your network drops fragmented UDP packets.  Otherwise,
+## leave it disabled — DNS over TCP is slower and less efficient.
+
+force_tcp = false
+
+
+## Enable *experimental* support for HTTP/3 (DoH3, HTTP over QUIC).
+## This requires a DoH server that supports HTTP/3, such as AdGuard DNS.
+## Note that, like DNSCrypt but unlike other HTTP/3 implementations,
+## DoH3 has perfect forward secrecy.
+
+http3 = false
+
+
+## SOCKS proxy
+## Uncomment the following line to route all TCP connections through a
+## SOCKS v5 proxy.  The proxy must support TCP connections on port 53.
+## The SOCKS proxy may throw an error if the domain name can't be
+## resolved; in that case, use IP addresses or add the domain to
+## the `forwarding_rules` file.
+
+# proxy = 'socks5://127.0.0.1:9050'
+
+
+## HTTP/HTTPS proxy
+## Uncomment the following line to route all TCP connections through an
+## HTTP/HTTPS proxy.  This is only useful for DoH servers.
+
+# http_proxy = 'http://127.0.0.1:8888'
+
+
+## How long a DNS query will wait for a response, in milliseconds.
+## This can be reduced to 2500 if all used servers are nearby.
+
+timeout = 5000
+
+
+## Keepalive for HTTP/HTTPS queries, in seconds.
+
+keepalive = 30
+
+
+## Response for blocked queries.  Options are `refused`, `hinfo` (which
+## causes browsers to stop trying to resolve the name) and `force`.
+## `force` returns the DNSSEC-signed response defined in the `[blocked_names]`
+## section, or a generic DNS response if there isn't one.
+
+# blocked_query_response = 'refused'
+
+
+## Load-balancing strategy: 'p2' (default), 'ph', 'p<n>', 'random' or 'fastest'.
+## The 'fastest' strategy requires the lb_estimator to be enabled.
+
+# lb_strategy = 'p2'
+
+
+## Enable to estimate the optimal load balancing strategy for the current
+## network conditions.
+
+# lb_estimator = true
+
+
+## Log level (0-6, default: 2 — 0 is very verbose, 6 only contains
+## fatal errors).
+
+# log_level = 2
+
+
+## Log file for the application, as an alternative to sending logs to
+## the standard error.  Leave empty to use the default,
+## which varies according to the operating system.
+
+# log_file = 'dnscrypt-proxy.log'
+
+
+## When using a log file, only keep logs from the most recent launch.
+
+# log_file_latest = true
+
+
+## Use the system logger (syslog on Unix, Event Log on Windows).
+
+# use_syslog = true
+
+
+## Delay, in minutes, after which certificates are reloaded.
+
+cert_refresh_delay = 240
+
+
+## DNSCrypt: Create a new, unique key for every single DNS query.
+## This may improve privacy but can also have a significant impact on
+## performance.  Enable only if you are using a server that doesn't
+## limit the number of queries per key.
+
+# dnscrypt_ephemeral_keys = false
+
+
+## DoH: Disable TLS session tickets — increases privacy but may also
+## increase latency.
+
+# tls_disable_session_tickets = false
+
+
+## DoH: Use a specific cipher suite instead of the server's preferred one.
+## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.
+## In some cases, this can significantly improve performance.
+
+# tls_cipher_suite = [52392, 49199]
+
+
+## DoH: Log TLS secret keys to a file for use with Wireshark.
+## Never enable this in a production environment.
+
+# tls_key_log_file = '/tmp/keylog.txt'
+
+
+## Bootstrap resolvers.
+##
+## These are normal DNS resolvers that will be initially used to resolve
+## the IP addresses of the DoH/ODoH/DoT resolvers you will be using,
+## and the IP addresses of DNS relays.
+##
+## These are only required for DNS stamps using DNS-over-HTTPS
+## (DoH) or DNS-over-TLS (DoT) for initial bootstrap.  DNSCrypt
+## stamps don't require them since they include IP addresses.
+##
+## Even if you don't use DoH/DoT, it's still useful to leave these
+## set to widely available resolvers, for reliability.
+
+bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53']
+
+
+## Always use the bootstrap resolver before the system DNS settings.
+## In some configurations, this is required to prevent the system DNS
+## configuration from being used before dnscrypt-proxy is ready.
+
+ignore_system_dns = true
+
+
+## The maximum time, in seconds, to wait for the network to be available
+## before giving up.
+
+netprobe_timeout = 60
+
+
+## Address and port to try initializing a UDP socket to, to check if the
+## network is up.
+
+netprobe_address = '9.9.9.9:53'
+
+
+## Automatic log files rotation.
+
+# Maximum log file size, in MiB.  The default is 10 MiB.
+log_files_max_size = 10
+
+# How long to keep log files, in days.
+log_files_max_age = 7
+
+# Maximum number of log files to keep.
+log_files_max_backups = 1
+
+
+
+#########################
+#        Filters        #
+#########################
+
+## Note: if you are using dnsmasq, disable all `block_*` filters — they
+## are applied before dnsmasq gets the queries.
+
+## Immediately respond to IPv6-related queries with an empty response.
+## This reduces load on internal resolvers.  But if you have IPv6, or
+## use Tor, disable it.
+
+block_ipv6 = false
+
+## Immediately respond to unqualified (single-label) names with an empty
+## response.  This prevents leakage on LLMNR and other protocols that
+## might not be under your control.
+
+block_unqualified = true
+
+## Immediately respond to undelegated (private) TLDs with an empty
+## response.
+
+block_undelegated = true
+
+## TTL for negative (blocked) responses.  The default is 10 seconds.
+
+reject_ttl = 10
+
+
+###############################
+#        Cloaking rules       #
+###############################
+
+## Cloaking returns a static IP address for a specific, configured set of
+## host names.  This can be used for parental control systems, to
+## redirect specific domains to trusted servers, or to reduce traffic
+## to known advertising trackers.
+
+# forwarding_rules = 'forwarding-rules.txt'
+
+
+## Cloaking rules file.  One rule per line, format:
+## <domain> <resolver-ip>.
+## The resolver IP must be a public DNS resolver reachable from the proxy.
+
+# cloaking_rules = 'cloaking-rules.txt'
+
+## TTL used for cloaked entries (default 600 seconds).
+# cloak_ttl = 600
+
+## Set to `true` to enable automatic reverse cloaking: if a DNS query
+## returns an IP address that is in the cloaking rules, the corresponding
+## domain name is returned instead.
+
+# cloak_ptr = false
+
+
+###########################
+#        DNS cache        #
+###########################
+
+## Enable a DNS cache to reduce latency and outgoing traffic.
+
+cache = true
+
+## Cache size, in number of entries.
+
+cache_size = 4096
+
+## Minimum TTL for cached entries, in seconds.
+
+cache_min_ttl = 2400
+
+## Maximum TTL for cached entries, in seconds.
+
+cache_max_ttl = 86400
+
+## Minimum TTL for negatively cached entries, in seconds.
+
+cache_neg_min_ttl = 60
+
+## Maximum TTL for negatively cached entries, in seconds.
+
+cache_neg_max_ttl = 600
+
+
+########################################
+#        Captive portal handling       #
+########################################
+
+[captive_portals]
+
+## A map file, one host name or regex per line, of sites that must be
+## accessible even if a schedule would otherwise block them.
+## This can be useful to allow access to a captive portal page when
+## connected to a public WiFi network.
+
+# map_file = 'example-captive-portals.txt'
+
+
+##################################
+#        Local DoH server        #
+##################################
+
+[local_doh]
+
+## dnscrypt-proxy can act as a local DoH server.  If this section is
+## present, a local DoH server will be started.
+
+## The address(es) and port(s) the local DoH server should listen to.
+## Only TCP is supported.
+
+# listen_addresses = ['127.0.0.1:3000']
+
+
+## Path of the DoH URL.  This is optional — if not specified, the
+## path will be '/dns-query'.
+
+# path = '/dns-query'
+
+
+## Certificate and key files for the local DoH server.  These must be
+## provided if the server is listening on a port different from 127.0.0.1.
+
+# cert_file = 'localhost.pem'
+# cert_key_file = 'localhost.pem'
+
+
+###############################
+#        Query logging        #
+###############################
+
+[query_log]
+
+  ## Path to the query log file.  Leave empty to disable query logging.
+  ## The format is one query per line, in the format specified below.
+
+  # file = 'query.log'
+
+
+  ## Query log format: 'tsv', 'ltsv', or 'json'.
+
+  format = 'tsv'
+
+
+  ## Do not log queries for these query types.
+  ## Use this to reduce the log size if you don't need to log all queries.
+
+  # ignored_qtypes = ['DNSKEY', 'NS']
+
+
+############################################
+#        Suspicious queries logging        #
+############################################
+
+[nx_log]
+
+  ## Path to the log file for suspicious queries.  Leave empty to disable.
+
+  # file = 'nx.log'
+
+
+  ## Query log format: 'tsv', 'ltsv', or 'json'.
+
+  format = 'tsv'
+
+
+######################################################
+#        Pattern-based blocking (blocklists)         #
+######################################################
+
+## Blocklists are made of one pattern per line.  A pattern is a domain
+## name, or a domain name with a wildcard prefix.
+## Example: `*.example.com` matches `www.example.com` and all other
+## subdomains of `example.com`.
+##
+## The path to the file is resolved from the dnscrypt-proxy working
+## directory when it is a relative path.
+
+[blocked_names]
+
+  ## Path to the file containing blocked names.
+
+  blocked_names_file = '/etc/dns/blocked-names.txt'
+
+
+  ## Optional path to a file logging blocked queries.
+
+  # log_file = 'blocked-names.log'
+
+
+  ## Optional log format: 'tsv', 'ltsv', or 'json'.
+
+  # log_format = 'tsv'
+
+
+###########################################################
+#        Pattern-based IP blocking (IP blocklists)        #
+###########################################################
+
+[blocked_ips]
+
+  ## Path to the file containing blocked IP addresses.
+
+  # blocked_ips_file = 'blocked-ips.txt'
+
+
+  ## Optional path to a file logging blocked queries.
+
+  # log_file = 'blocked-ips.log'
+
+
+  ## Optional log format: 'tsv', 'ltsv', or 'json'.
+
+  # log_format = 'tsv'
+
+
+######################################################
+#   Pattern-based allow lists (blocklists bypass)    #
+######################################################
+
+[allowed_names]
+
+  ## Path to the file containing allowed names.
+
+  # allowed_names_file = 'allowed-names.txt'
+
+
+  ## Optional path to a file logging allowed queries.
+
+  # log_file = 'allowed-names.log'
+
+
+  ## Optional log format: 'tsv', 'ltsv', or 'json'.
+
+  # log_format = 'tsv'
+
+
+#########################################################
+#   Pattern-based allowed IPs lists (blocklists bypass) #
+#########################################################
+
+[allowed_ips]
+
+  ## Path to the file containing allowed IP addresses.
+
+  # allowed_ips_file = 'allowed-ips.txt'
+
+
+  ## Optional path to a file logging allowed queries.
+
+  # log_file = 'allowed-ips.log'
+
+
+  ## Optional log format: 'tsv', 'ltsv', or 'json'.
+
+  # log_format = 'tsv'
+
+
+##########################################
+#        Time access restrictions        #
+##########################################
+
+[schedules]
+
+  ## Schedule of the day during which DNS queries should be blocked.
+  ## Times are in UTC.
+
+  [schedules.time-to-sleep]
+    mon = [{after='21:00', before='5:00'}]
+    tue = [{after='21:00', before='5:00'}]
+    wed = [{after='21:00', before='5:00'}]
+    thu = [{after='21:00', before='5:00'}]
+    fri = [{after='21:00', before='5:00'}]
+    sat = [{after='21:00', before='5:00'}]
+    sun = [{after='21:00', before='5:00'}]
+
+  # [schedules.work]
+  #   mon = [{after='9:00', before='18:00'}]
+  #   tue = [{after='9:00', before='18:00'}]
+  #   wed = [{after='9:00', before='18:00'}]
+  #   thu = [{after='9:00', before='18:00'}]
+  #   fri = [{after='9:00', before='17:00'}]
+
+
+#########################
+#        Servers        #
+#########################
+
+## Remote servers (DNS stamps, from DNSCrypt, DoH and DoT servers)
+## are loaded from remote sources.
+
+[sources]
+
+  ## An example of a remote source.
+
+  [sources.public-resolvers]
+    urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
+    cache_file = 'public-resolvers.md'
+    minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
+    refresh_delay = 73
+    prefix = ''
+
+  ## An example of a remote source for relays.
+
+  [sources.relays]
+    urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md']
+    cache_file = 'relays.md'
+    minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
+    refresh_delay = 73
+    prefix = ''
+
+  ## ODoH (Oblivious DoH) servers.
+
+  # [sources.odoh-servers]
+  #   urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md']
+  #   cache_file = 'odoh-servers.md'
+  #   minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
+  #   refresh_delay = 73
+  #   prefix = ''
+
+  ## ODoH (Oblivious DoH) relays.
+
+  # [sources.odoh-relays]
+  #   urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md']
+  #   cache_file = 'odoh-relays.md'
+  #   minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
+  #   refresh_delay = 73
+  #   prefix = ''
+
+  ## Quad9
+
+  # [sources.quad9-resolvers]
+  #   urls = ['https://www.quad9.net/quad9-resolvers.md']
+  #   minisign_key = 'RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJNMKqv=m7wzB2pLDkVF'
+  #   cache_file = 'quad9-resolvers.md'
+  #   refresh_delay = 73
+  #   prefix = 'quad9-'
+
+  ## Planet Freeze
+
+  # [sources.dnscry-pt-resolvers]
+  #   urls = ['https://www.dnscry.pt/resolvers.md']
+  #   cache_file = 'dnscry-pt-resolvers.md'
+  #   refresh_delay = 73
+  #   prefix = 'dnscry-pt-'
+
+  ## Parental Control
+
+  # [sources.parental-control]
+  #   urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md']
+  #   cache_file = 'parental-control.md'
+  #   minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
+  #   refresh_delay = 73
+  #   prefix = ''
+
+
+#########################################
+#        Servers with known bugs        #
+#########################################
+
+[broken_implementations]
+
+  ## Cisco servers cannot handle fragmented queries larger than 1500
+  ## bytes, which is way below the expected minimal MTU.
+
+  fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cisco-sandbox', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6']
+
+
+#################################################################
+#        Certificate-based client authentication for DoH        #
+#################################################################
+
+## Use X.509 certificates to authenticate DNS-over-HTTPS clients.
+
+[doh_client_x509_auth]
+
+  ## Enable DNS over HTTPS client authentication via X.509 certificates.
+  ## This is optional — if not present, all clients will be allowed.
+
+  # creds = [
+  #   {
+  #     client_id = 'client1'
+  #     server_name = 'server1.com'
+  #     cert_file = 'client1.pem'
+  #     key_file = 'client1.key'
+  #     ca_file = 'ca.pem'
+  #   }
+  # ]
+
+
+################################
+#        Anonymized DNS        #
+################################
+
+## Anonymized DNS relays.
+##
+## DNS queries can be sent to a DNS relay, that will forward them to a
+## DNS server.  This is useful to hide your IP address from the DNS
+## server, and to bypass IP-based rate limiting.
+##
+## An Anonymized DNS relay is a server that implements the Anonymized
+## DNSCrypt protocol.  It is not a DNS resolver.
+
+[anonymized_dns]
+
+  ## Routes are indirect ways to reach a DNS server.  Each route is
+  ## a list of relay names, followed by the server name.
+  ##
+  ## A route can be as simple as a single relay, or can be a chain of
+  ## relays.  If a chain of relays is used, the last relay in the chain
+  ## will connect to the DNS server.
+  ##
+  ## Empty routes are ignored.
+
+  # routes = [
+  #   { server_name='example-server', via=['anon-example-1', 'anon-example-2'] }
+  # ]
+
+
+  ## Skip resolvers incompatible with anonymization instead of using them
+  ## without the relay.
+
+  skip_incompatible = false
+
+  ## If a relay is not available, try to connect to the DNS server
+  ## directly.  This is the default.
+
+  # direct_cert_fallback = false
+
+
+###############################
+#            DNS64            #
+###############################
+
+## DNS64 enables IPv6-only networks to reach IPv4-only destinations.
+## This is useful for mobile networks, and for networks that are
+## transitioning to IPv6.
+
+[dns64]
+
+  ## IPv6 prefix to use for DNS64 synthesis.
+
+  # prefix = ['64:ff9b::/96']
+
+  ## DNS64 resolver(s) to use.  These are regular DNS resolvers that
+  ## will be used to resolve the IPv4 addresses.
+
+  # resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53']
+
+
+########################################
+#            Static entries            #
+########################################
+
+## Static entries can be used to define custom DNS records.
+
+[static]
+
+  ## A static entry is a DNS record that is always returned for a
+  ## specific domain name, without querying an upstream server.
+
+  # [static.myserver]
+  #   stamp = 'sdns://...'
diff --git a/tests/dnscrypt.scm b/tests/dnscrypt.scm
new file mode 100644 (file)
index 0000000..a486ab2
--- /dev/null
@@ -0,0 +1,28 @@
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; dnscrypt-proxy test cases.
+
+(define-module (tests dnscrypt)
+  #:use-module (guix gexp)
+  #:export (dnscrypt-test-cases))
+
+(define (dnscrypt-test-cases marionette)
+  "Return a gexp with dnscrypt-proxy test assertions."
+  #~(begin
+      (test-assert "dnscrypt: service running"
+                   (marionette-eval '(begin
+                                       (use-modules (gnu services herd))
+                                       (start-service 'dnscrypt-proxy))
+                                    #$marionette))
+
+      (test-assert "dnscrypt: port 53 TCP"
+                   (wait-for-tcp-port 53
+                                      #$marionette))
+
+      ;; Verify resolv.conf was installed by the service.
+      ;; Its content is fixed (nameserver 127.0.0.1), so
+      ;; existence alone validates it.
+      (test-assert "dnscrypt: resolv.conf exists"
+                   (marionette-eval '(file-exists? "/etc/resolv.conf")
+                                    #$marionette))))
diff --git a/tests/ntp.scm b/tests/ntp.scm
new file mode 100644 (file)
index 0000000..8f3fc25
--- /dev/null
@@ -0,0 +1,17 @@
+;;; Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+;;; License: GPL-3.0 or later.
+;;;
+;;; NTP test cases.
+
+(define-module (tests ntp)
+  #:use-module (guix gexp)
+  #:export (ntp-test-cases))
+
+(define (ntp-test-cases marionette)
+  "Return a gexp with NTP daemon test assertion."
+  #~(begin
+      (test-assert "ntp: ntpd running"
+                   (marionette-eval '(begin
+                                       (use-modules (gnu services herd))
+                                       (start-service 'ntpd))
+                                    #$marionette))))
index 33fa71ac4e78306979293d6c5f53b27fd8a1f8ad..a074d196f3ed3a014b9244b1fcf5afb485d0694b 100644 (file)
   #:use-module (gnu packages ssh)
   #:use-module (vps-system)
   #:use-module (vps-home)
+  #:use-module (tests sshd)
+  #:use-module (tests networking)
+  #:use-module (tests ntp)
   #:use-module (tests claude-code)
   #:use-module (tests dotfiles)
   #:use-module (tests paseo)
-  #:use-module (tests sshd)
-  #:use-module (tests networking)
+  #:use-module (tests dnscrypt)
   #:use-module (guix gexp)
   #:export (%test-vps %vps-test-os run-vps-test))
 
@@ -187,24 +189,27 @@ every function in TEST-CASES.  Each element is a function
                                     #$marionette-gexp))))
 
 ;;;
-;;; System test record — test cases are added incrementally in
-;;; subsequent commits.
+;;; System test record.
 ;;;
 
 (define %test-vps
-  (let ((claude claude-code-test-cases)
-        (paseo paseo-test-cases)
-        (dots dotfiles-test-cases)
-        (home home-activation-test-cases)
-        (ssh (lambda (m)
+  (let ((ssh (lambda (m)
                (sshd-test-cases m
                                 (make-ssh-connect m))))
-        (net networking-test-cases))
+        (net networking-test-cases)
+        (ntp ntp-test-cases)
+        (dns dnscrypt-test-cases)
+        (claude claude-code-test-cases)
+        (paseo paseo-test-cases)
+        (dots dotfiles-test-cases)
+        (home home-activation-test-cases))
     (system-test (name "vps")
                  (description "VPS test suite.")
-                 (value (run-vps-test (list claude
+                 (value (run-vps-test (list ssh
+                                            net
+                                            ntp
+                                            dns
+                                            claude
                                             paseo
                                             dots
-                                            home
-                                            ssh
-                                            net))))))
+                                            home))))))
index 7eb60b9bb0e540099d81fcede43e9956d6491b0c..2258573d03843c26623c1ad3793abb5d5c7be57a 100644 (file)
@@ -12,6 +12,7 @@
   #:use-module (gnu packages bash)
   #:use-module (nongnu packages linux)
   #:use-module (conf vps sshd)
+  #:use-module (conf vps dnscrypt)
   #:export (vps-operating-system))
 
 (use-service-modules networking shepherd)
                     (supplementary-groups '("wheel"))
                     (home-directory "/home/dak")
                     (shell (file-append bash "/bin/bash")))
+                  (user-account
+                    (name "dnscrypt-proxy")
+                    (group "dnscrypt-proxy")
+                    (system? #t)
+                    (home-directory "/var/empty")
+                    (shell (file-append shadow "/sbin/nologin")))
                   %base-user-accounts))
 
+    (groups (cons (user-group
+                    (name "dnscrypt-proxy")
+                    (system? #t)) %base-groups))
+
     ;; Services
     
     (services
               %ssh-service
 
               ;; NTP — correct time is required for Guix substitutes
-              (service ntp-service-type)) %base-services))
+              (service ntp-service-type)
+
+              ;; dnscrypt-proxy — encrypted DNS proxy
+              (simple-service 'dnscrypt-proxy-daemon
+                              shepherd-root-service-type
+                              (list dnscrypt-proxy-shepherd-service))
+
+              ;; Point system resolver to localhost.
+              ;; Uses a dedicated service type that extends
+              ;; etc-service-type rather than being one.
+              (service resolv-conf-service-type)
+
+              ;; TODO: nginx stream for DNS-over-TLS on port 853.
+              ;; See server.git dnscrypt-proxy/dnscrypt-proxy.conf
+              ;; and the metadata.git Ansible playbook for the
+              ;; envsubst-based deployment pattern (SSL certs needed).
+              ) %base-services))
 
     ;; Sudoers: dak can reconfigure without a password