Skip to content

Commit 954ef98

Browse files
committed
WIP on adding more DBus calls
1 parent 301efe8 commit 954ef98

12 files changed

+731
-91
lines changed

HACKING.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ and [linking them to Nix packages](https://nixos.org/manual/nixos/stable/index.h
6464
This project's NixOS test sets up three machines:
6565

6666
1. An OpenVPN server,
67-
2. An OpenVPN client, and
68-
3. A DNS resolver running [Dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html).
67+
2. An OpenVPN client,
68+
3. A DNS resolver running an instance of
69+
[dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) bound only to the
70+
loopback address, plus an instance of
71+
[RouteDNS](https://github.com/folbricht/routedns) bound to an address
72+
reachable by the other machines.
6973

7074
The OpenVPN server and client run a [point-to-point configuration with a static
7175
key](https://openvpn.net/community-resources/static-key-mini-howto/). The
@@ -77,6 +81,10 @@ and then asserts that certain hostnames are resolvable _from the client_ that
7781
would not be resolvable unless the client were configured to use the DNS
7882
settings specified in its OpenVPN configuration file.
7983

84+
The resolver machine uses dnsmasq for actual name resolution, plus DNSSEC
85+
validation. The RouteDNS instance running on the same machine terminates
86+
DNS-over-TLS and forwards queries to Dnsmasq.
87+
8088
#### Extending the NixOS test
8189

8290
If you are implementing a new feature or correcting a bug in
@@ -113,6 +121,10 @@ $ nix build -L '.#checks.x86_64-linux.update-systemd-resolved'
113121
[^supported-systems]: Run `nix flake show` to view flake outputs namespaced by
114122
all supported systems.
115123

124+
#### Maintaining NixOS test assets
125+
126+
##### Regenerating the DNS-over-TLS keypair
127+
116128
### Entering the Nix development shell
117129

118130
To enter the Nix development shell, run the following command:
@@ -127,3 +139,7 @@ shell.
127139
#### Summary of available commands
128140

129141
- `fmt`: format all Nix code in this project using [`alejandra`](https://github.com/kamadorueda/alejandra).
142+
- `mkdotcert`: regenerate the keypair used for encrypting DNS-over-TLS in the
143+
NixOS system test.
144+
- `mkanchor`: regenerate the DNSSEC trust anchors configuration used with
145+
dnsmasq in the NixOS system test.

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,15 @@ OpenVPN, either through the server, or the client, configuration:
307307
| `DOMAIN` or `ADAPTER_DOMAIN_SUFFIX` | `example.com` | The primary domain for this host. If set multiple times, the first provided is used as the primary search domain for bare hostnames. Any subsequent `DOMAIN` options will be added as the equivalent of `DOMAIN-SEARCH` options. All requests for this domain as well will be routed to the `DNS` servers provided on this link. | [SetLinkDomains][resolved] |
308308
| `DOMAIN-SEARCH` | `example.com` | Secondary domains which will be used to search for bare hostnames (after any `DOMAIN`, if set) and in the order provided. All requests for this domain will be routed to the `DNS` servers provided on this link. | [SetLinkDomains][resolved] |
309309
| `DOMAIN-ROUTE` | `example.com` | All requests for these domains will be routed to the `DNS` servers provided on this link. They will *not* be used to search for bare hostnames, only routed. A `DOMAIN-ROUTE` option for `.` (single period) will instruct `systemd-resolved` to route the entire DNS name-space through to the `DNS` servers configured for this connection (unless a more specific route has been offered by another connection for a selected name/name-space). This is useful if you wish to prevent [DNS leakage](#dns-leakage). | [SetLinkDomains][resolved] |
310-
| `DNSSEC` | `yes`</br >`default` | Control of DNSSEC should be enabled (`yes`) or disabled (`no`), or `allow-downgrade` to switch off DNSSEC only if the server doesn't support it, for any queries over this link only, or use the system default (`default`). | [SetLinkDNSSEC][resolved] |
310+
| `DNSSEC` | `yes`, `true`</br >`no`, `false`</br >`default`</br >`allow-downgrade` | Control of DNSSEC should be enabled (`yes`, `true`) or disabled (`no`, `false`), or `allow-downgrade` to switch off DNSSEC only if the server doesn't support it, for any queries over this link only, or use the system default (`default`). | [SetLinkDNSSEC][resolved] |
311+
| `FLUSH-CACHES` | `yes`, `true`</br >`no`, `false` | Whether or not to flush all local DNS caches. Enabled by default. | [FlushCaches][resolved] |
312+
| `RESET-SERVER-FEATURES` | `yes`, `true`</br >`no`, `false` | Whether or not to forget learnt DNS server feature levels. Disabled by default. | [ResetServerFeatures][resolved] |
313+
| `RESET-STATISTICS` | `yes`, `true`</br >`no`, `false` | Whether or not to reset resolver statistics. Disabled by default. | [ResetStatistics][resolved] |
314+
| `DEFAULT-ROUTE` | `yes`, `true`</br >`no`, `false` | If true, this link's configured DNS servers are used for resolving domain names that do not match any link's configured Domains= setting. If false, this link's configured DNS servers are never used for such domains, and are exclusively used for resolving names that match at least one of the domains configured on this link. Disabled by default. | [DNSDefaultRoute][resolved] |
315+
| `DNS-OVER-TLS` | `yes`, `true`</br >`no`, `false`</br >`opportunistic` | If true all connections to the server will be encrypted. Note that this mode requires a DNS server that supports DNS-over-TLS and has a valid certificate. If the hostname was specified in DNS= by using the format "address#server_name" it is used to validate its certificate and also to enable Server Name Indication (SNI) when opening a TLS connection. Otherwise the certificate is checked against the server's IP. If the DNS server does not support DNS-over-TLS all DNS requests will fail. When set to "opportunistic" DNS request are attempted to send encrypted with DNS-over-TLS. If the DNS server does not support TLS, DNS-over-TLS is disabled. Note that this mode makes DNS-over-TLS vulnerable to "downgrade" attacks, where an attacker might be able to trigger a downgrade to non-encrypted mode by synthesizing a response that suggests DNS-over-TLS was not supported. If set to false, DNS lookups are send over UDP. | [SetLinkDNSOverTLS][resolved] |
316+
| `LLMNR` | `yes`, `true`</br >`no`, `false`</br >`resolve` | Takes a boolean or "resolve". When true, enables Link-Local Multicast Name Resolution[1] on the link. When set to "resolve", only resolution is enabled, but not host registration and announcement. Defaults to true. | [SetLinkLLMNR][resolved] |
317+
| `MULTICAST-DNS` | `yes`, `true`</br >`no`, `false`</br >`resolve` | When true, enables Multicast DNS support on the link. When set to "resolve", only resolution is enabled, but not host or service registration and announcement. Defaults to false. | [SetLinkMulticastDNS][resolved] |
318+
| `DNSSEC-NEGATIVE-TRUST-ANCHORS` | `trusted.org` | If specified and DNSSEC is enabled, look-ups done via the interface's DNS server will be subject to the list of negative trust anchors, and not require authentication for the specified domains, or anything below it. Use this to disable DNSSEC authentication for specific private domains, that cannot be proven valid using the Internet DNS hierarchy. Defaults to the empty list. | [SetLinkDNSSECNegativeTrustAnchors][resolved] |
311319

312320
**Note**: There are no local or system options to be configured. All configuration
313321
for this script is handled through OpenVPN, including, for example, the name of

nix/checks.nix

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@
8282
nodes = {
8383
resolver = {
8484
networking.domain = vpnDomain;
85+
86+
networking.firewall = {
87+
#allowedTCPPorts = [resolverPort 5353];
88+
#allowedUDPPorts = [resolverPort 5353];
89+
allowedTCPPorts = [5353];
90+
allowedUDPPorts = [5353];
91+
};
92+
8593
services.dnsmasq = {
8694
enable = true;
8795
resolveLocalQueries = false;
@@ -118,12 +126,48 @@
118126
"server-cname,server-cname.${vpnDomain},server"
119127
"client-cname,client-cname.${vpnDomain},client"
120128
];
129+
130+
dnssec = true;
131+
trust-anchor = lib.importJSON ./trust-anchor.json;
121132
};
122133
};
123134

124-
networking.firewall = {
125-
allowedTCPPorts = [resolverPort];
126-
allowedUDPPorts = [resolverPort];
135+
services.routedns = {
136+
enable = true;
137+
138+
settings = {
139+
resolvers = {
140+
local-tcp = {
141+
address = "127.0.0.1:53";
142+
protocol = "tcp";
143+
};
144+
145+
local-udp = {
146+
address = "127.0.0.1:53";
147+
protocol = "udp";
148+
};
149+
};
150+
151+
listeners = let
152+
commonConfig = {
153+
address = ":5353";
154+
155+
# Generated with `mkcert`.
156+
server-crt = ./resolver.crt;
157+
server-key = ./resolver.key;
158+
};
159+
in {
160+
local-dot = commonConfig // {
161+
protocol = "dot";
162+
resolver = "local-tcp";
163+
};
164+
165+
local-dtls = commonConfig // {
166+
protocol = "dtls";
167+
resolver = "local-udp";
168+
};
169+
};
170+
};
127171
};
128172
};
129173

@@ -176,8 +220,14 @@
176220
polkitRules = mkPolkitRulesForService config.systemd.services.${serviceAttrName};
177221
in {
178222
networking.useNetworkd = true;
179-
services.resolved.enable = true;
180-
services.resolved.dnssec = "false";
223+
224+
services.resolved = {
225+
enable = true;
226+
dnssec = "false"; # overridden for VPN interface
227+
extraConfig = ''
228+
MulticastDNS=no
229+
'';
230+
};
181231

182232
users.users.openvpn = {
183233
description = "openvpn client user";
@@ -218,14 +268,29 @@
218268
219269
config ${perSystem.config.packages.update-systemd-resolved}/libexec/openvpn/update-systemd-resolved.conf
220270
221-
dhcp-option DNS ${resolverIP}
271+
dhcp-option DNS ${resolverIP}:5353#resolver
222272
dhcp-option DOMAIN ${vpnDomain}
273+
274+
dhcp-option FLUSH-CACHES yes
275+
dhcp-option RESET-SERVER-FEATURES true
276+
dhcp-option RESET-STATISTICS yes
277+
278+
dhcp-option DEFAULT-ROUTE yes
279+
dhcp-option DNS-OVER-TLS yes
280+
dhcp-option LLMNR resolve
281+
dhcp-option MULTICAST-DNS default
282+
283+
dhcp-option DNSSEC true
284+
dhcp-option DNSSEC-NEGATIVE-TRUST-ANCHORS ${vpnDomain}
223285
'';
224286
};
225287

226288
# Add our generated ruleset to the system's polkit rules
227289
environment.etc."polkit-1/rules.d/10-update-systemd-resolved.rules".source = polkitRules;
228290

291+
# `mkcert` CA
292+
security.pki.certificateFiles = [./rootCA.pem];
293+
229294
security.polkit = {
230295
enable = true;
231296
debug = true;
@@ -292,6 +357,11 @@
292357
machine.execute('systemctl status -l {0} 1>&2'.format(unit))
293358
raise(e)
294359
360+
def dump_resolved_info(machine):
361+
with machine.nested('printing resolved status and statistics'):
362+
machine.succeed('resolvectl status 1>&2')
363+
machine.succeed('resolvectl statistics 1>&2')
364+
295365
def assert_hostname_match(machine, expected, *args):
296366
cmd = shlex.join(['dig', '+short', *args])
297367
@@ -313,6 +383,18 @@
313383
with machine.nested('checking that hostname resolves to expected address "{0}" from {1}'.format(expected, machine.name)):
314384
retry(hostname_matches)
315385
386+
def extract_interface_property(machine, interface, property, *args):
387+
with machine.nested('extracting property "{0}" of interface "{1}"'.format(property, interface)):
388+
cmd = shlex.join(['resolvectl', *args, property, interface])
389+
return machine.succeed("{0} | grep -m1 -Po '(?<=:\s).*'".format(cmd)).rstrip()
390+
391+
def assert_interface_property(machine, interface, property, expected, *args):
392+
with machine.nested('checking that property "{0}" of interface "{1}" is "{2}"'.format(property, interface, expected)):
393+
actual = extract_interface_property(machine, interface, property, *args)
394+
machine.log('property "{0}" of interface "{1}" is "{2}"'.format(property, interface, actual))
395+
if actual != expected:
396+
raise Exception('expected property "{0}" of interface "{1}" to be "{2}", but got "{3}"'.format(property, interface, expected, actual))
397+
316398
# Machine.wait_for_open_port only checks ports on localhost
317399
def wait_for_open_host_port(machine, host, port, extra=[]):
318400
cmd = shlex.join(['nc'] + extra + ['-z', host, str(port)])
@@ -330,20 +412,37 @@
330412
331413
resolver.start()
332414
wait_for_unit_with_output(resolver, 'dnsmasq')
415+
wait_for_unit_with_output(resolver, 'routedns')
333416
334417
server.start()
335418
wait_for_unit_with_output(server, '${serviceName}')
336419
337420
client.start()
421+
338422
wait_for_unit_with_output(client, '${serviceName}')
339423
340424
# Block until we can reach the resolver (or until we hit the retry
341425
# timeout). Pass `-u` flag to check UDP port; also check TCP port.
342-
wait_for_open_host_port(client, '${resolverIP}', 53, extra=['-u'])
343-
wait_for_open_host_port(client, '${resolverIP}', 53)
426+
#wait_for_open_host_port(client, '${resolverIP}', 53, extra=['-u'])
427+
#wait_for_open_host_port(client, '${resolverIP}', 53)
428+
wait_for_open_host_port(client, '${resolverIP}', 5353, extra=['-u'])
429+
wait_for_open_host_port(client, '${resolverIP}', 5353)
344430
345431
assert_hostname_match(client, '${resolverIP}', 'resolver-cname.${vpnDomain}')
346432
assert_hostname_match(client, '${serverIP}', 'server-cname.${vpnDomain}')
433+
434+
dump_resolved_info(client)
435+
436+
assert_interface_property(client, '${interface}', 'default-route', 'yes')
437+
assert_interface_property(client, '${interface}', 'llmnr', 'resolve')
438+
assert_interface_property(client, '${interface}', 'mdns', 'no')
439+
assert_interface_property(client, '${interface}', 'dnsovertls', 'yes')
440+
assert_interface_property(client, '${interface}', 'dnssec', 'yes')
441+
442+
client.succeed('systemctl restart ${serviceName}')
443+
wait_for_unit_with_output(client, '${serviceName}')
444+
445+
dump_resolved_info(client)
347446
'';
348447
};
349448
};

nix/devshells.nix

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@
1414
exec ${config.treefmt.build.wrapper}/bin/treefmt "$@"
1515
'';
1616
}
17+
18+
{
19+
name = "mkdotcert";
20+
category = "maintenance";
21+
help = "Generate the DNS-over-TLS keypair for use in system testing";
22+
command = let
23+
inherit (config.checks.update-systemd-resolved.nodes) resolver;
24+
in ''
25+
export CAROOT="''${PRJ_ROOT:-.}/nix"
26+
${pkgs.mkcert}/bin/mkcert -install || exit
27+
${pkgs.mkcert}/bin/mkcert \
28+
-cert-file "''${CAROOT}/resolver.crt" \
29+
-key-file "''${CAROOT}/resolver.key" \
30+
${resolver.networking.hostName} \
31+
${resolver.networking.hostName}.${resolver.networking.domain}
32+
'';
33+
}
34+
35+
{
36+
name = "mkanchor";
37+
category = "maintenance";
38+
help = "Fetch DNSSEC root anchors and translate them to dnsmasq format";
39+
command = ''
40+
${pkgs.xidel}/bin/xidel \
41+
--input-format xml \
42+
--output-format json-wrapped \
43+
-e 'for $kd in //TrustAnchor/KeyDigest return string-join((//TrustAnchor/Zone, $kd/KeyTag, $kd/Algorithm, $kd/DigestType, $kd/Digest), ",")' \
44+
https://data.iana.org/root-anchors/root-anchors.xml \
45+
| ${pkgs.jq}/bin/jq flatten > "''${PRJ_ROOT}/nix/trust-anchor.json"
46+
'';
47+
}
1748
];
1849

1950
devshell = {

nix/resolver.crt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEFTCCAn2gAwIBAgIQavGAznzoeOanX8TOEcjvqjANBgkqhkiG9w0BAQsFADBP
3+
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsMCW1hdHRA
4+
c3J2MTEZMBcGA1UEAwwQbWtjZXJ0IG1hdHRAc3J2MTAeFw0yMzA3MjUwMjQxMzha
5+
Fw0yNTEwMjUwMjQxMzhaMD0xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj
6+
ZXJ0aWZpY2F0ZTESMBAGA1UECwwJbWF0dEBzcnYxMIIBIjANBgkqhkiG9w0BAQEF
7+
AAOCAQ8AMIIBCgKCAQEA1IJRUwXZPs/5MSwePreyKNoIAZtgJ2Rx+NGZWWEw/CAK
8+
+rJBCCQDDbV2g7wS9i23tDeTO/2nMltYJgxmC3XYz0GCCspBUzRBxdyX32qQfx0w
9+
Jch9QA9TcBEnPJ+aljbxmqFMGtWJrrUkq3G0NxGmyTDQ0W8/i8kwKTlI9DOk0FSR
10+
TnZlKhREfgbW6QJ0yPu1cbbRCucOiB7ALbMdl47YG8a0UjrXcCCJYa2lWW3NbE7R
11+
RTAjFnS2khTwTqTwDpugyONLIIFN2tqWOU/J0/1CNvx0zZiR7k9YcWCpyveOnxtr
12+
nyRmCFGCicYRzX7PM2+LDB7n6+8X6blZ33fxVktjaQIDAQABo38wfTAOBgNVHQ8B
13+
Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUaZsI266/
14+
+Vb53ggYcWMMblNm6VwwNQYDVR0RBC4wLIIIcmVzb2x2ZXKCIHJlc29sdmVyLnVw
15+
ZGF0ZS5zeXN0ZW1kLnJlc29sdmVkMA0GCSqGSIb3DQEBCwUAA4IBgQAo0jhIlCof
16+
spCNraN+NMHowQttPCVTloqbXb5KlVC7a3+VffEZNFZM1VLkgkYqT3muy1zPoyQy
17+
PeoYapRR/3JY1/ws7hZ5bs4+xL1Bg74I4A9WbWKjJcqfHCOhZ7EigVJZc2FM45jP
18+
9OrJQh9lMBfSAaYsrgnHa/JJe9p3gYA8SD1Y9fxXPPKct7m8+Yi4Lxqh3Q9hvQLv
19+
5hWHkoGhbTDCYSrP/vJHlVKep0aTVo2pS09QvdqsYU5Mzf/KnvL9bVTCh+DY9IH+
20+
a5Izz9ss2d7OzVixax6AE5Xqsy2daq1RCXWnzJW8WRIZsnHJgFvXcHUhV15uJ05c
21+
sqpKsI+m8fCbva3d64/jMUlQ7CVobUAhzJ/+deqSpIIDMGZpQyJvY14zE4JfAokG
22+
tDcWfgGHpPnlVkWbzxiqPaXN7zXXFNDhOhAloE3Vg4JnG6dUE/6mFtWJlAo83qbb
23+
o5yKq3d48wI78yb/5lz+rx6ud+0Zr77A1kDk8ol3O6lOIxNO9KGCFYw=
24+
-----END CERTIFICATE-----

nix/resolver.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUglFTBdk+z/kx
3+
LB4+t7Io2ggBm2AnZHH40ZlZYTD8IAr6skEIJAMNtXaDvBL2Lbe0N5M7/acyW1gm
4+
DGYLddjPQYIKykFTNEHF3JffapB/HTAlyH1AD1NwESc8n5qWNvGaoUwa1YmutSSr
5+
cbQ3EabJMNDRbz+LyTApOUj0M6TQVJFOdmUqFER+BtbpAnTI+7VxttEK5w6IHsAt
6+
sx2XjtgbxrRSOtdwIIlhraVZbc1sTtFFMCMWdLaSFPBOpPAOm6DI40sggU3a2pY5
7+
T8nT/UI2/HTNmJHuT1hxYKnK946fG2ufJGYIUYKJxhHNfs8zb4sMHufr7xfpuVnf
8+
d/FWS2NpAgMBAAECggEAOEqM4EEcWtccWzokiNiACPI4TLSrs8OXrSFYTaTBJQgX
9+
4HB3aYCgjnETA7I+E5fooYRXK/z03RH1N57xKPf+hmgD2nfY9gFRqufUEwpXXFSO
10+
/HMvOljU8UqZ6iUc/c1wElXHoxQNdInnPMLRygSS+ZhEuDWPz6draoASIx3K+qPw
11+
Xq9j4ObVZepBSIA7bS/Sl/uFs/eUrx5dAG3Kcf/PJvV2eVV0Yx7R75C7+CNs+192
12+
EuYY9N8pwhcHtDVIzHXlheL/+VhBk1v8Xb8BfEgxjEZdUpe2AqG20nZwzkgHxFOa
13+
/r30dwRiRXBJILqQiaWI1mKeXVJ6qrXuLhyk8PuwAQKBgQDdnQ2gaZHG2L/a/Cb0
14+
W1KgAer1FO0dzbWHkAGhQuix3VLdIpLQnlo4GP6IWBC2NHltHeDzyprdkYKuGenz
15+
n7dh0hrLRMzhrLCKl6NkAcvDyOVTezCKSpNOUwYNR3lHeTKY+8Q3kqOEclQ+UPeg
16+
RSy+K5LwhVxy29cSKvuaxflxiQKBgQD1e54N4/q6zaECQU2kGBUmT0OEbtHwKFD5
17+
31zba2JqkW/eC9pRA/vNaiYYZDu/Ons9MBiBtxYIpQNeHh/HUeO+rBlXkMn37JuJ
18+
9FKDnp1aQ71UY8rDiPV4eqYQw8afrlqO4y9LK+82Vmd3N5Pn2WiDy2D0VZKG5Q6B
19+
sBQ90xVK4QKBgQDTKwQBBpdR0td94yd7UEm7DhjEz9vhulJvilkDQK5aTXrYHEmp
20+
YDq3mZlwcfn6pKXPw9jGdRh8aFsNasPy0Q38uCev6S8RG2xdo4Cdmth/Br7+fTQT
21+
klwrFhF+NczqviHohH7ENYZ6fjan6p8KqN+plfu+FFWzXKfjN/Hn2R2HgQKBgAPV
22+
6qJM7Z39mIZwfsYRmkL++g8XrDAUcS92Tf0fsGn528WcaczaQxTyk6XN6yERyNsr
23+
5TYhpjZ8XZEa52Q141kXV04G9SDqkYOWTbPAxrSiWlL3PDPR8APx5qZcaL4V+1RA
24+
OHz0MsimkPdL5wO4YemtQ9aNf7yb154vIiHVKoABAoGBAKqddjgcJd5Xcick+l2L
25+
EL1noAqpi7tUo+wdnKjYFIaNCqbRBz336DWWE8urxHoEbSBajt6oDuErYAVXBiIm
26+
fvhoQOhLJ2fzeVd3GbFUJd/ccb06vNaU3PAOkEKDlVShH4rSzz+Ms+FEiE5EIrhI
27+
pOoPAyw27H6Afgf9EXWPXxfG
28+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)