├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ ├── bug-report.yml │ └── support-request.yml └── workflows │ └── test.yml ├── tests ├── 01_no_updates.sh ├── 02_single_ipv4_dns.sh ├── 04a_single_dns_domain.sh ├── 06a_single_domain_route.sh ├── 15_single_ipv6_dns_localhost.sh ├── 05e_dns_domain_alternate.sh ├── 14_single_ipv6_dns_compact_3.sh ├── 13_single_ipv6_dns_compact_2.sh ├── 03a_multiple_ipv4_dns_1.sh ├── 11_single_ipv6_dns_simple.sh ├── 23_resolve1_dbus_presence.sh ├── 10_single_ipv6_dns_full.sh ├── 12_single_ipv6_dns_compact_1.sh ├── 04b_multiple_dns_domains.sh ├── 21_dnssec_invalid_options.sh ├── 05a_dns_domain_and_search_1.sh ├── 03b_multiple_ipv4_dns_2.sh ├── 16_dual_ipv6_single_ipv4.sh ├── 16a_dual_ipv6_single_ipv4.sh ├── 20_dnssec_only.sh ├── 06b_multiple_domain_routes.sh ├── 08a_dns_ipv4_and_domain.sh ├── 05b_dns_domain_and_search_2.sh ├── 05c_dns_domain_and_search_3.sh ├── 17_single_ipv6_single_ipv4.sh ├── 05d_dns_domain_and_search_4.sh ├── 07a_dns_domain_search_and_route_1.sh ├── 08b_dns_ipv4_domain_and_search.sh ├── 08b_dns_ipv4_domain_search_and_route.sh ├── 07b_dns_domain_search_and_route_2.sh ├── 22_dns_dnssec_domain_and_search.sh ├── 18_dns_ipv4_ipv6_domain_and_search.sh ├── helpers │ ├── ipv4.sh │ ├── ipv6.sh │ ├── foreign_options.sh │ └── assertions.sh ├── 19c_dns_invalid_ipv4.sh ├── 19a_dns_invalid_ipv6.sh ├── 24_custom_foreign_options.sh └── 19b_dns_valid_ipv6.sh ├── nix ├── trust-anchor.json ├── openvpn.key.static ├── resolver.crt ├── rootCA.pem ├── resolver.key ├── rootCA-key.pem ├── devshells.nix ├── packages.nix ├── nixos-modules.nix └── checks.nix ├── update-systemd-resolved.conf ├── .editorconfig ├── LICENSE ├── flake.nix ├── .shellcheckrc ├── Makefile ├── flake.lock ├── HACKING.md ├── run-tests ├── CHANGELOG.md ├── docs └── nixos-modules.md ├── README.md └── update-systemd-resolved /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[a-z0-9] 2 | .sw[a-z0-9] 3 | *~ 4 | 5 | # Nix build results 6 | result 7 | result-* 8 | repl-result-* 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Allow users to create issues without using one of the templates 2 | blank_issues_enabled: true 3 | -------------------------------------------------------------------------------- /tests/01_no_updates.sh: -------------------------------------------------------------------------------- 1 | # Emulate OpenVPN environment 2 | script_type="up" 3 | 4 | TEST_TITLE="No Updates" 5 | TEST_BUSCTL_CALLED=1 6 | -------------------------------------------------------------------------------- /tests/02_single_ipv4_dns.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 1.23.4.56") 4 | 5 | TEST_TITLE="Single IPv4 DNS Server" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 2 4 1 23 4 56" 8 | -------------------------------------------------------------------------------- /tests/04a_single_dns_domain.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DOMAIN example.com") 4 | 5 | TEST_TITLE="Single DNS Domain" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DOMAINS="1 example.com false" 8 | -------------------------------------------------------------------------------- /tests/06a_single_domain_route.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DOMAIN-ROUTE example.com") 4 | 5 | TEST_TITLE="Single DNS Route" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DOMAINS="1 example.com true" 8 | -------------------------------------------------------------------------------- /tests/15_single_ipv6_dns_localhost.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS ::1") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Localhost)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1" 8 | -------------------------------------------------------------------------------- /tests/05e_dns_domain_alternate.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option ADAPTER_DOMAIN_SUFFIX example.org") 4 | 5 | TEST_TITLE="DNS Doamin using ADAPTER_DOMAIN_SUFFIX" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DOMAINS="1 example.org false" 8 | -------------------------------------------------------------------------------- /tests/14_single_ipv6_dns_compact_3.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 20a0::1") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Compact) (Part 3)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 32 160 0 0 0 0 0 0 0 0 0 0 0 0 0 1" 8 | -------------------------------------------------------------------------------- /nix/trust-anchor.json: -------------------------------------------------------------------------------- 1 | [ 2 | ".,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5", 3 | ".,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D", 4 | ".,38696,8,2,683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16" 5 | ] -------------------------------------------------------------------------------- /tests/13_single_ipv6_dns_compact_2.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 1234:567:89::ab:cdef") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Compact) (Part 2)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239" 8 | -------------------------------------------------------------------------------- /tests/03a_multiple_ipv4_dns_1.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 5.6.7.89" 6 | ) 7 | 8 | TEST_TITLE="Multiple IPv4 DNS Servers (Part 1)" 9 | TEST_BUSCTL_CALLED=1 10 | TEST_BUSCTL_DNS="2 2 4 1 23 4 56 2 4 5 6 7 89" 11 | -------------------------------------------------------------------------------- /tests/11_single_ipv6_dns_simple.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 1234:567:89:0:ab:cde:f123:4567") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Full, Simple)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 18 52 5 103 0 137 0 0 0 171 12 222 241 35 69 103" 8 | -------------------------------------------------------------------------------- /tests/23_resolve1_dbus_presence.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | TEST_TITLE='Error if "busctl status org.freedesktop.resolve1" fails' 4 | TEST_BUSCTL_CALLED=1 5 | 6 | # Mocked-up busctl function will return exit code 1 upon "busctl status <...>" 7 | TEST_BUSCTL_STATUS_RC=1 8 | EXPECT_FAILURE=1 9 | -------------------------------------------------------------------------------- /tests/10_single_ipv6_dns_full.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 1234:5678:90ab:cdef:4321:8765:ba09:fedc") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Full)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 18 52 86 120 144 171 205 239 67 33 135 101 186 9 254 220" 8 | -------------------------------------------------------------------------------- /tests/12_single_ipv6_dns_compact_1.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=("dhcp-option DNS 1234:567:89:0:ab:cde:f123:4567") 4 | 5 | TEST_TITLE="Single IPv6 DNS Server (Compact) (Part 1)" 6 | TEST_BUSCTL_CALLED=1 7 | TEST_BUSCTL_DNS="1 10 16 18 52 5 103 0 137 0 0 0 171 12 222 241 35 69 103" 8 | -------------------------------------------------------------------------------- /tests/04b_multiple_dns_domains.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN example.com" 5 | "dhcp-option DOMAIN example.co" 6 | ) 7 | 8 | TEST_TITLE="Multiple DNS Domains" 9 | TEST_BUSCTL_CALLED=1 10 | TEST_BUSCTL_DOMAINS="2 example.com false example.co false" 11 | -------------------------------------------------------------------------------- /tests/21_dnssec_invalid_options.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/foreign_options.sh" 2 | 3 | script_type="up" 4 | 5 | TEST_BUSCTL_CALLED=0 6 | EXPECT_FAILURE=1 7 | 8 | for test_value in 1 0 DOWNGRADE; do 9 | run_custom_foreign_option_test_with_ip_ifindex DNSSEC "$test_value" "$test_value" 10 | done 11 | -------------------------------------------------------------------------------- /tests/05a_dns_domain_and_search_1.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN example.com" 5 | "dhcp-option DOMAIN-SEARCH example.org" 6 | ) 7 | 8 | TEST_TITLE="DNS Single Domain and Single Search" 9 | TEST_BUSCTL_CALLED=1 10 | TEST_BUSCTL_DOMAINS="2 example.com false example.org false" 11 | -------------------------------------------------------------------------------- /tests/03b_multiple_ipv4_dns_2.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 5.6.7.89" 6 | "dhcp-option DNS 34.5.67.8" 7 | ) 8 | 9 | TEST_TITLE="Multiple IPv4 DNS Servers (Part 2)" 10 | TEST_BUSCTL_CALLED=1 11 | TEST_BUSCTL_DNS="3 2 4 1 23 4 56 2 4 5 6 7 89 2 4 34 5 67 8" 12 | -------------------------------------------------------------------------------- /tests/16_dual_ipv6_single_ipv4.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1234:567:89::ab:cdef" 5 | "dhcp-option DNS 1.23.4.56" 6 | ) 7 | 8 | TEST_TITLE="Single IPv6 and Single IPv4 DNS Servers" 9 | TEST_BUSCTL_CALLED=1 10 | TEST_BUSCTL_DNS="2 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239 2 4 1 23 4 56" 11 | -------------------------------------------------------------------------------- /tests/16a_dual_ipv6_single_ipv4.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS6 1234:567:89::ab:cdef" 5 | "dhcp-option DNS 1.23.4.56" 6 | ) 7 | 8 | TEST_TITLE="Single IPv6 and Single IPv4 DNS Servers (DNS6)" 9 | TEST_BUSCTL_CALLED=1 10 | TEST_BUSCTL_DNS="2 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239 2 4 1 23 4 56" 11 | -------------------------------------------------------------------------------- /tests/20_dnssec_only.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/foreign_options.sh" 2 | 3 | script_type="up" 4 | 5 | for test_value in true false yes no default allow-downgrade; do 6 | run_custom_foreign_option_test_with_ip_ifindex DNSSEC "$test_value" "$test_value" 7 | run_custom_foreign_option_test_with_ip_ifindex DNSSEC "${test_value^}" "$test_value" 8 | done 9 | -------------------------------------------------------------------------------- /tests/06b_multiple_domain_routes.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN-ROUTE example.com" 5 | "dhcp-option DOMAIN-ROUTE example.co" 6 | "dhcp-option DOMAIN-ROUTE example.co.uk" 7 | ) 8 | 9 | TEST_TITLE="Single DNS Route" 10 | TEST_BUSCTL_DOMAINS="3 example.com true example.co true example.co.uk true" 11 | TEST_BUSCTL_CALLED=1 12 | -------------------------------------------------------------------------------- /tests/08a_dns_ipv4_and_domain.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 2.34.56.7" 6 | "dhcp-option DOMAIN example.com" 7 | ) 8 | 9 | TEST_TITLE="DNS IPv4 Servers and Domain" 10 | TEST_BUSCTL_CALLED=1 11 | TEST_BUSCTL_DNS="2 2 4 1 23 4 56 2 4 2 34 56 7" 12 | TEST_BUSCTL_DOMAINS="1 example.com false" 13 | -------------------------------------------------------------------------------- /tests/05b_dns_domain_and_search_2.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN example.com" 5 | "dhcp-option DOMAIN-SEARCH example.org" 6 | "dhcp-option DOMAIN-SEARCH example.net" 7 | ) 8 | 9 | TEST_TITLE="DNS Single Domain and Dual Search" 10 | TEST_BUSCTL_CALLED=1 11 | TEST_BUSCTL_DOMAINS="3 example.com false example.org false example.net false" 12 | -------------------------------------------------------------------------------- /update-systemd-resolved.conf: -------------------------------------------------------------------------------- 1 | script-security 2 2 | up /usr/local/libexec/openvpn/update-systemd-resolved 3 | up-restart 4 | down /usr/local/libexec/openvpn/update-systemd-resolved 5 | down-pre 6 | 7 | # If needed, to permit `update-systemd-resolved` to find utilities it depends 8 | # on. Adjust to suit your system. 9 | #setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 10 | -------------------------------------------------------------------------------- /tests/05c_dns_domain_and_search_3.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN-SEARCH example.org" 5 | "dhcp-option DOMAIN example.com" 6 | "dhcp-option DOMAIN-SEARCH example.net" 7 | ) 8 | 9 | TEST_TITLE="DNS Single Domain and Dual Search (with Order Check)" 10 | TEST_BUSCTL_CALLED=1 11 | TEST_BUSCTL_DOMAINS="3 example.com false example.org false example.net false" 12 | -------------------------------------------------------------------------------- /tests/17_single_ipv6_single_ipv4.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1234:567:89::ab:cdef" 5 | "dhcp-option DNS 1.23.4.56" 6 | "dhcp-option DNS 20a0::1" 7 | ) 8 | 9 | TEST_TITLE="Single IPv6 and Single IPv4 DNS Servers" 10 | TEST_BUSCTL_CALLED=1 11 | TEST_BUSCTL_DNS="3 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239 2 4 1 23 4 56 10 16 32 160 0 0 0 0 0 0 0 0 0 0 0 0 0 1" 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | [{update-systemd-resolved,run-tests,*.sh}] 17 | switch_case_indent = true 18 | space_redirects = true 19 | -------------------------------------------------------------------------------- /tests/05d_dns_domain_and_search_4.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN-SEARCH example.org" 5 | "dhcp-option DOMAIN example.co" 6 | "dhcp-option DOMAIN example.com" 7 | "dhcp-option DOMAIN-SEARCH example.net" 8 | ) 9 | 10 | TEST_TITLE="DNS Dual Domain and Dual Search (with Order Check)" 11 | TEST_BUSCTL_CALLED=1 12 | TEST_BUSCTL_DOMAINS="4 example.co false example.org false example.com false example.net false" 13 | -------------------------------------------------------------------------------- /tests/07a_dns_domain_search_and_route_1.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN example.com" 5 | "dhcp-option DOMAIN-SEARCH example.org" 6 | "dhcp-option DOMAIN-SEARCH example.co.uk" 7 | "dhcp-option DOMAIN-ROUTE example.net" 8 | ) 9 | 10 | TEST_TITLE="DNS Single Domain, Dual Search, Single Route" 11 | TEST_BUSCTL_CALLED=1 12 | TEST_BUSCTL_DOMAINS="4 example.com false example.org false example.co.uk false example.net true" 13 | -------------------------------------------------------------------------------- /tests/08b_dns_ipv4_domain_and_search.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 2.34.5.67" 6 | "dhcp-option DOMAIN example.co.uk" 7 | "dhcp-option DOMAIN-SEARCH example.co" 8 | "dhcp-option DOMAIN-SEARCH example.com" 9 | ) 10 | 11 | TEST_TITLE="DNS IPv4 Servers, Domain, and Search" 12 | TEST_BUSCTL_CALLED=1 13 | TEST_BUSCTL_DNS="2 2 4 1 23 4 56 2 4 2 34 5 67" 14 | TEST_BUSCTL_DOMAINS="3 example.co.uk false example.co false example.com false" 15 | -------------------------------------------------------------------------------- /tests/08b_dns_ipv4_domain_search_and_route.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 2.34.5.67" 6 | "dhcp-option DOMAIN example.co.uk" 7 | "dhcp-option DOMAIN-SEARCH example.co" 8 | "dhcp-option DOMAIN-ROUTE example.com" 9 | ) 10 | 11 | TEST_TITLE="DNS IPv4 Servers, Domain, Search, and Route" 12 | TEST_BUSCTL_CALLED=1 13 | TEST_BUSCTL_DNS="2 2 4 1 23 4 56 2 4 2 34 5 67" 14 | TEST_BUSCTL_DOMAINS="3 example.co.uk false example.co false example.com true" 15 | -------------------------------------------------------------------------------- /tests/07b_dns_domain_search_and_route_2.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DOMAIN example.com" 5 | "dhcp-option DOMAIN-SEARCH example.org" 6 | "dhcp-option DOMAIN-ROUTE example.net" 7 | "dhcp-option DOMAIN-SEARCH example.co.uk" 8 | "dhcp-option DOMAIN example.co" 9 | "dhcp-option DOMAIN-ROUTE example.uk.com" 10 | ) 11 | 12 | TEST_TITLE="DNS Dual Domain, Dual Search, Dual Route (with Order Check)" 13 | TEST_BUSCTL_CALLED=1 14 | TEST_BUSCTL_DOMAINS="6 example.com false example.org false example.co.uk false example.co false example.net true example.uk.com true" 15 | -------------------------------------------------------------------------------- /tests/22_dns_dnssec_domain_and_search.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 1234:567:89::ab:cdef" 6 | "dhcp-option DOMAIN example.com" 7 | "dhcp-option DOMAIN-SEARCH example.org" 8 | "dhcp-option DOMAIN-ROUTE example.net" 9 | "dhcp-option DNSSEC yes" 10 | ) 11 | 12 | TEST_TITLE="DNS, DNSSEC, Domain, Search, and Route" 13 | TEST_BUSCTL_CALLED=1 14 | TEST_BUSCTL_DOMAINS="3 example.com false example.org false example.net true" 15 | TEST_BUSCTL_DNSSEC="yes" 16 | TEST_BUSCTL_DNS="2 2 4 1 23 4 56 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239" 17 | -------------------------------------------------------------------------------- /tests/18_dns_ipv4_ipv6_domain_and_search.sh: -------------------------------------------------------------------------------- 1 | script_type="up" 2 | 3 | foreign_options=( 4 | "dhcp-option DNS 1.23.4.56" 5 | "dhcp-option DNS 2.34.56.7" 6 | "dhcp-option DNS 1234:567:89::ab:cdef" 7 | "dhcp-option DNS 1234:567:89::ba:cdef" 8 | "dhcp-option DOMAIN example.com" 9 | "dhcp-option DOMAIN-SEARCH example.co" 10 | ) 11 | 12 | TEST_TITLE="DNS IPv4 and IPv6 Servers, plus Domain and Search" 13 | TEST_BUSCTL_CALLED=1 14 | TEST_BUSCTL_DNS="4 2 4 1 23 4 56 2 4 2 34 56 7 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 171 205 239 10 16 18 52 5 103 0 137 0 0 0 0 0 0 0 186 205 239" 15 | TEST_BUSCTL_DOMAINS="2 example.com false example.co false" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /nix/openvpn.key.static: -------------------------------------------------------------------------------- 1 | # 2 | # 2048 bit OpenVPN static key 3 | # 4 | -----BEGIN OpenVPN Static key V1----- 5 | 837007a0f24e279f3d4f1f982c49b85f 6 | 3f49e0a1e4b9eedafe354ad3727d0a9f 7 | 2eba6053fd580ffd525268bebfecb5b2 8 | 32bde652b196b3ee2296b9c29d80a98b 9 | 240f961e6c8384f45ae5648af0c0cf91 10 | e5695a901035a812dc203daf5bb2283e 11 | f382f1114c5f37c67422609f6ab95d89 12 | 97174eb5b00f6848ac3bb7e36d7d09b3 13 | 5b9321201a483fc1a9722f92f2d614d1 14 | 5ec8ab69c090ed3154c7dcb2361be126 15 | 1c61fdf572cfe5e34bdd882cd3ee0204 16 | 2f350acc52d5e88efdfc2277145a3d02 17 | 9ce86929e1c9aa4482c2c469e8d76c13 18 | 100c5b5313edf48be200e9f64ca59cb6 19 | feb1e914edfc4bdcbbc1487dbb8aae4a 20 | deac011f5a4140494362f58a03ab919a 21 | -----END OpenVPN Static key V1----- 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 'Feature Request' 2 | description: 'Request a new feature or improvement' 3 | title: '[Idea]: ' 4 | labels: 5 | - 'Enhancement' 6 | body: 7 | - type: textarea 8 | id: idea 9 | attributes: 10 | label: 'Your request.' 11 | description: 'How could `update-systemd-resolved` improve?' 12 | - type: markdown 13 | attributes: 14 | value: >- 15 | Thank you for your feedback. 16 | 17 | `update-systemd-resolved` welcomes pull requests. If you would like to 18 | contribute, please see [the README's "How to help" section](https://github.com/jonathanio/update-systemd-resolved#how-to-help) 19 | and [the development notes](https://github.com/jonathanio/update-systemd-resolved/blob/master/HACKING.md). 20 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Description for the project"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | devshell.url = "github:numtide/devshell"; 8 | devshell.inputs.nixpkgs.follows = "nixpkgs"; 9 | 10 | flake-parts.url = "github:hercules-ci/flake-parts"; 11 | 12 | treefmt-nix.url = "github:numtide/treefmt-nix"; 13 | treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; 14 | }; 15 | 16 | outputs = inputs @ {flake-parts, ...}: 17 | flake-parts.lib.mkFlake {inherit inputs;} ({lib, ...}: { 18 | systems = lib.subtractLists [ 19 | "armv5tel-linux" 20 | "armv6l-linux" 21 | "mipsel-linux" 22 | "riscv64-linux" 23 | ] (lib.intersectLists lib.systems.flakeExposed lib.platforms.linux); 24 | 25 | imports = [ 26 | inputs.devshell.flakeModule 27 | inputs.treefmt-nix.flakeModule 28 | inputs.flake-parts.flakeModules.easyOverlay 29 | ./nix/checks.nix 30 | ./nix/devshells.nix 31 | ./nix/nixos-modules.nix 32 | ./nix/packages.nix 33 | ]; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # Test suite `source`-es a bunch of files; those files declare (without the 2 | # `export` builtin) a number of variables recognized by `run-tests` and/or 3 | # `update-systemd-resolved` itself. We therefore disable `SC2034` globally -- 4 | # this is the check that complains: 5 | # 6 | # SC2034 (warning): appears unused Verify use (or export if used externally) 7 | # 8 | # This is more convenient than disabling it at every location where shellcheck 9 | # would otherwise complain, and avoids the useless/misleading use of the 10 | # `export` builtin. 11 | disable=SC2034 12 | 13 | # Follow `source` statements even if they refer to files not specified as 14 | # inputs to the `shellcheck` command. 15 | external-sources=true 16 | 17 | # Scripts in `./tests` don't have shebangs. Tell shellcheck that we're 18 | # targeting Bash. 19 | shell=bash 20 | 21 | # Tell shellcheck to resolve paths like `./foo/bar.sh` relative to the sourcing 22 | # script. That is, if `/corge/grault/quux.sh` contains the command `source 23 | # ./foo/bar.sh`, `source-path=SCRIPTDIR` makes shellcheck treat this as 24 | # equivalent to `source /corge/grault/foo/bar.sh`. 25 | source-path=SCRIPTDIR 26 | -------------------------------------------------------------------------------- /tests/helpers/ipv4.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/assertions.sh" 2 | 3 | source update-systemd-resolved 4 | 5 | declare -a ipv4_expansion_implementations 6 | 7 | add_available_ipv4_expansion_implementations() { 8 | if (("$2" == 1)); then 9 | ipv4_expansion_implementations+=("$1") 10 | else 11 | warning "IPv4 expansion implementation '$1' is not available" 12 | fi 13 | 14 | # Returning 0 short-circuits each_ip_expansion 15 | return 1 16 | } 17 | 18 | each_ip_expansion_func add_available_ipv4_expansion_implementations IPv4 || exit 19 | 20 | all_ipv4_expansion_implementations() { 21 | local -a expansions=() 22 | local description="Address: ${1?}" 23 | local expansion 24 | local implementation_func 25 | 26 | for implementation_func in "${ipv4_expansion_implementations[@]}"; do 27 | expansion="$("$implementation_func" "${1?}")" || : 28 | expansion="${expansion:-}" 29 | expansions+=("$expansion") 30 | description="${description:+${description} | }$(printf -- '%s: %s' "$implementation_func" "$expansion")" 31 | done 32 | 33 | if ! all_pairs_equal "${expansions[@]}"; then 34 | printf -- '%s\n' "$description" 35 | return 1 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /tests/helpers/ipv6.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/assertions.sh" 2 | 3 | source update-systemd-resolved 4 | 5 | declare -a ipv6_expansion_implementations 6 | 7 | add_available_ipv6_expansion_implementations() { 8 | if (("$2" == 1)); then 9 | ipv6_expansion_implementations+=("$1") 10 | else 11 | warning "IPv6 expansion implementation '$1' is not available" 12 | fi 13 | 14 | # Returning 0 short-circuits each_ip_expansion 15 | return 1 16 | } 17 | 18 | each_ip_expansion_func add_available_ipv6_expansion_implementations IPv6 || exit 19 | 20 | all_ipv6_expansion_implementations() { 21 | local -a expansions=() 22 | local description="Address: ${1?}" 23 | local expansion 24 | local implementation_func 25 | 26 | for implementation_func in "${ipv6_expansion_implementations[@]}"; do 27 | expansion="$("$implementation_func" "${1?}")" || : 28 | expansion="${expansion:-}" 29 | expansions+=("$expansion") 30 | description="${description:+${description} | }$(printf -- '%s: %s' "$implementation_func" "$expansion")" 31 | done 32 | 33 | if ! all_pairs_equal "${expansions[@]}"; then 34 | printf -- '%s\n' "$description" 35 | return 1 36 | fi 37 | } 38 | -------------------------------------------------------------------------------- /tests/helpers/foreign_options.sh: -------------------------------------------------------------------------------- 1 | # Convenience function for testing the behaviour of `update-systemd-resolved`'s 2 | # custom foreign option. Defines a number of variables recognized by 3 | # `run-test` and its `busctl` mock function. 4 | run_custom_foreign_option_test() { 5 | local directive="$1" 6 | shift 7 | 8 | local args="$1" 9 | shift 10 | 11 | # If we're here, we expect `busctl` to be called (unless told otherwise). 12 | TEST_BUSCTL_CALLED="${TEST_BUSCTL_CALLED:-1}" 13 | 14 | local dhcp_option="${directive}${args:+ ${args}}" 15 | local TEST_TITLE="resolved-specific dhcp-option directive: ${directive}${args:+ ${args}}" 16 | local foreign_option_1="dhcp-option ${dhcp_option}" 17 | local suffix="${directive//-/_}" 18 | local varname="TEST_BUSCTL_${suffix^^}" 19 | declare "${varname}=$*" 20 | 21 | runtest 22 | } 23 | 24 | # `run_custom_foreign_option_test` convenience wrapper that injects the 25 | # interface index as the first argument in the list of expected arguments. 26 | run_custom_foreign_option_test_with_ip_ifindex() { 27 | local directive="$1" 28 | shift 29 | 30 | local args="$1" 31 | shift 32 | 33 | run_custom_foreign_option_test "$directive" "$args" "${ip_ifindex?}" "$@" 34 | } 35 | -------------------------------------------------------------------------------- /tests/19c_dns_invalid_ipv4.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/ipv4.sh" 2 | 3 | script_type="up" 4 | 5 | # busctl should not be called for any test in here 6 | TEST_BUSCTL_CALLED=0 7 | 8 | cases=( 9 | 'nope' 10 | '192.168.22' 11 | '1.1.1.1.' 12 | '.9.9.9.9' 13 | '1.2.3.999' 14 | 'x.x.x.x' 15 | '1.2.3.-4' 16 | ) 17 | 18 | all_ipv4_addresses_invalid() { 19 | local -a improperly_accepted_ipv4=() 20 | local -a wrongly_parsed_ipv4=() 21 | local ipv4 bad status 22 | 23 | for ipv4 in "${cases[@]}"; do 24 | foreign_option_1="dhcp-option DNS ${ipv4}" 25 | 26 | if source update-systemd-resolved; then 27 | improperly_accepted_ipv4+=("$ipv4") 28 | fi 29 | 30 | if ! bad="$(all_ipv4_expansion_implementations "$ipv4")"; then 31 | wrongly_parsed_ipv4+=("$bad") 32 | fi 33 | done 34 | 35 | if ((${#improperly_accepted_ipv4[@]} > 0)); then 36 | printf 1>&2 -- 'improperly accepted the following ipv4 addresses:\n' 37 | printf 1>&2 -- ' %s\n' "${improperly_accepted_ipv4[@]}" 38 | status=1 39 | fi 40 | 41 | if ((${#wrongly_parsed_ipv4[@]} > 0)); then 42 | printf 1>&2 -- 'parse for the following ipv4 addresses wrongly succeeded:\n' 43 | 44 | for bad in "${wrongly_parsed_ipv4[@]}"; do 45 | printf 1>&2 -- ' %s\n' "$bad" 46 | done 47 | 48 | status=1 49 | fi 50 | 51 | return "${status:-0}" 52 | } 53 | 54 | TEST_TITLE="Known-bad IPv4 addresses are all rejected" 55 | checktest all_ipv4_addresses_invalid 56 | -------------------------------------------------------------------------------- /tests/helpers/assertions.sh: -------------------------------------------------------------------------------- 1 | # Assert that a condition holds for all supplied arguments 2 | all() { 3 | if (("$#" < 2)); then 4 | printf 1>&2 -- 'Usage: %s [ ...]\n' "${FUNCNAME[0]}" 5 | return 64 # EX_USAGE 6 | fi 7 | 8 | local cond="$1" 9 | shift 10 | 11 | local arg 12 | for arg in "$@"; do 13 | "$cond" "$arg" || return 14 | done 15 | } 16 | 17 | # Assert that a binary comparison holds across all pairs within a list 18 | all_pairs() { 19 | if (("$#" < 2)); then 20 | printf 1>&2 -- 'Usage: %s [ ...]\n' "${FUNCNAME[0]}" 21 | return 64 # EX_USAGE 22 | fi 23 | 24 | local all_pairs_cond="$1" 25 | shift 26 | 27 | # If the list is empty or has only one element, then the comparison condition 28 | # cannot fail 29 | if (("$#" < 2)); then 30 | return 0 31 | fi 32 | 33 | local last 34 | 35 | # This is called indirectly. 36 | # shellcheck disable=SC2317 37 | __all_pairs_cond() { 38 | if [[ -v last ]]; then 39 | "$all_pairs_cond" "$last" "${1?internal error}" || return 40 | fi 41 | 42 | last="$1" 43 | } 44 | 45 | all __all_pairs_cond "$@" 46 | } 47 | 48 | # Assert that the provided arguments are (stringly) equal 49 | equal() { 50 | if (("$#" != 2)); then 51 | printf 1>&2 -- 'Usage: %s \n' "${FUNCNAME[0]}" 52 | return 64 # EX_USAGE 53 | fi 54 | 55 | [[ $1 == "$2" ]] 56 | } 57 | 58 | # Assert that all elements in a list are (stringly) equal 59 | all_pairs_equal() { 60 | all_pairs equal "$@" 61 | } 62 | -------------------------------------------------------------------------------- /nix/resolver.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEFTCCAn2gAwIBAgIQKwvXzQu/4HatRb/O3hbBMzANBgkqhkiG9w0BAQsFADBP 3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsMCW1hdHRA 4 | c3J2MTEZMBcGA1UEAwwQbWtjZXJ0IG1hdHRAc3J2MTAeFw0yNTAzMjIxNDQ2MzFa 5 | Fw0yNzA2MjIxNDQ2MzFaMD0xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj 6 | ZXJ0aWZpY2F0ZTESMBAGA1UECwwJbWF0dEBzcnYxMIIBIjANBgkqhkiG9w0BAQEF 7 | AAOCAQ8AMIIBCgKCAQEA5+iP+XBfx7qDVhMuq6OUUjy/Rrq73HnnTHSFhoMCRk2y 8 | 2HCZ789mwNB2sEp1FLxWJGqvwJNelrrhijpeiC+OUzozCAjjVQJjPdYd5z0kVlLr 9 | cEQ/eJVDobEBv5ljY4eZ+BCTwOalYCsgxBnlIFOl4/9H9BleEfijNfMkdarC8i6x 10 | 3R+XN9QArhMjMOnlVDovVp6tawBYcdae38+ulV/Wrxp+YixNNb601RQ8x3sK/4vw 11 | +mAg//8gfKKnU423OoxOaeo02GiWX9lpXTQ+xQHrANXeXaEEahdQ3x6a84qoAS6A 12 | piOWjxnzOjwjm2tlvOHksXbwKgpSLnxI71Ad0oC5sQIDAQABo38wfTAOBgNVHQ8B 13 | Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUaZsI266/ 14 | +Vb53ggYcWMMblNm6VwwNQYDVR0RBC4wLIIIcmVzb2x2ZXKCIHJlc29sdmVyLnVw 15 | ZGF0ZS5zeXN0ZW1kLnJlc29sdmVkMA0GCSqGSIb3DQEBCwUAA4IBgQC3t2kvUEHt 16 | dMAWbvm2E3ZAUbZcIDbJ2Ifl7GbyIvGcrkknoA1leAnmEEKtIGAaPXzGFeiWk3U0 17 | Tjx6zsdDPGYyWkWU3v4hLNZ0Dm8Jb2ViuDe3IyWfvsAFOr0kRXzlonMup8qd5MZD 18 | 87r8+MDNmSwQjSvCPDFWGeVMjApAPDfddomw+WqsjVsO4VRnPmji+sCyFffjk2KL 19 | A1xVT7KpuGWjI2ewdl+C9phs8vy5tRxpW5TPA2LECjIKHQvs7J1iXCSuhhLu4bCd 20 | V41emSitLSruAl5RkQsg47xB7rBSchMDsooUgBgfQt5Dal8+9aL9PdQDz9vFM+ko 21 | yZaYEbc/VCA9qv5XoIKfPZrScDmNGS8z8hG8z5HmQsyiivP2IOMPWS0HW4FXmHGa 22 | TKr3W69/Fh7xHoTBbzANnSHzjib1OmPLvR0MZdtW0+JNrkIthEWtL0Zak68bofod 23 | AQzHvzUzL7Is2/cHzS6DqhJemF6+bdOvkJW/gUn1zjWLX1lsUn4JYeA= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run update-systemd-resolved tests 2 | on: 3 | pull_request: 4 | push: 5 | workflow_dispatch: 6 | jobs: 7 | native: 8 | name: Run tests on native architecture 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install testing dependencies 13 | run: | 14 | sudo apt-get -y update 15 | sudo apt-get -y install python3 sipcalc 16 | - name: Run tests 17 | run: ./run-tests 18 | 19 | cross: 20 | name: Run tests on ${{ matrix.arch }} architecture 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | arch: 25 | - aarch64 26 | - ppc64le 27 | steps: 28 | - uses: actions/checkout@v4 29 | # https://github.com/marketplace/actions/run-on-architecture 30 | - uses: uraimo/run-on-arch-action@v3 31 | name: Run tests 32 | with: 33 | arch: ${{ matrix.arch }} 34 | distro: ubuntu22.04 35 | githubToken: ${{ github.token }} 36 | install: | 37 | apt-get -y update 38 | apt-get -y install python3 sipcalc 39 | run: ./run-tests 40 | flake: 41 | name: Run Nix flake checks 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: cachix/install-nix-action@v31 46 | with: 47 | extra_nix_config: | 48 | system-features = benchmark big-parallel kvm nixos-test uid-range 49 | - name: run flake checks 50 | run: nix flake check -L 51 | -------------------------------------------------------------------------------- /nix/rootCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEbTCCAtWgAwIBAgIQJuzkRpIw4Nb8IP+EHDo4YTANBgkqhkiG9w0BAQsFADBP 3 | MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsMCW1hdHRA 4 | c3J2MTEZMBcGA1UEAwwQbWtjZXJ0IG1hdHRAc3J2MTAeFw0yMzA3MjQwMzIxNTZa 5 | Fw0zMzA3MjQwMzIxNTZaME8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBD 6 | QTESMBAGA1UECwwJbWF0dEBzcnYxMRkwFwYDVQQDDBBta2NlcnQgbWF0dEBzcnYx 7 | MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzDUm8gXNF1MjDyTx1REa 8 | fs3PLyRxWQ1ntjtDVMytuL6HYakRLNfsct0IBiHQk1Vl3PX1xTgF+kwHJIVpD+I9 9 | 64j73oggo57CoIGx76A0pTajVXBgFZzpxibTNUf/vOR2H6pe4no8eXrU5uYq38Aq 10 | b8OPSN172nlVoN7IgEsb5cbd/fm8/SSIp9++2LD/XPW8c5U9pah1i/rl1elzwb86 11 | 4ygtm1r+wYmmapvpD78bFMaqk/j1vaKglmsusOrIS57yAkcJHtGDT4tiPWKe9SNk 12 | DE7/yLl41DqcovHWS0WAdlk9IDr53QQmDYTJ2xPJpiaGYp2TMeAgju7D0PaVEZR1 13 | 707b0xMk1nRxPc7sHqYeOZyUq3ou+rlB8pREK6qAJ0ipFsnwQSsUtGDW5yFiJqHb 14 | CH3sSKUrh4RaIamZ1KbPCiinuPlGrapO1SkwrrOGpcyEOFbNRm9STdxmHaqt5Lzr 15 | Sz4kHuu1sx0CfdY1xPRw60MfhKk83wHjnaHlXuuBKvKpAgMBAAGjRTBDMA4GA1Ud 16 | DwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRpmwjbrr/5 17 | VvneCBhxYwxuU2bpXDANBgkqhkiG9w0BAQsFAAOCAYEAFvt/JwKE74yKnjIKZYLF 18 | h5+jEcTg9qH9TFBgJTDM2WgXU3d+hDKYXIEm/D1mAPmNTalVPpoJPGZAUWerHJFk 19 | YAYg/rncG6hS14jFr4adkadYfTHHWeWwci2wWh/CykV3+A/+zG8fHKwKwNVaylYM 20 | //oYLFHGbba3pVbIxcRpyy7QiFy/bE1Hweat75EJscJIw+8CzWRTn76J/JvEwvj6 21 | pCWdfD3iVgqdU/iloy5dh4LEJADlcvdjAsZVnIJKTeGlR7HGMloS4ThK92zuTZvF 22 | ST1i6HWHJ4UV7Tk/FB8jl0Lovo9kf0JSzFPgs+ynPtw/zLrI9q+4xndrJfXrVR/e 23 | K/fjoSeFprXeAu+nIDd3Y39iUQQz2x4sXSRk5E7vMD9YVUuHBKGNmWz16NuWXGNw 24 | 2IVXKscRr5YlD+dqtKYZg4cvko/mDmm3QmMqPweq+r9Y9G/1r0YPencCupbOPGHf 25 | SuTLZjkPDhZSzgZfxMb0KAtgomDOQvfjXimDPEFEF5wi 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /nix/resolver.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDn6I/5cF/HuoNW 3 | Ey6ro5RSPL9GurvceedMdIWGgwJGTbLYcJnvz2bA0HawSnUUvFYkaq/Ak16WuuGK 4 | Ol6IL45TOjMICONVAmM91h3nPSRWUutwRD94lUOhsQG/mWNjh5n4EJPA5qVgKyDE 5 | GeUgU6Xj/0f0GV4R+KM18yR1qsLyLrHdH5c31ACuEyMw6eVUOi9Wnq1rAFhx1p7f 6 | z66VX9avGn5iLE01vrTVFDzHewr/i/D6YCD//yB8oqdTjbc6jE5p6jTYaJZf2Wld 7 | ND7FAesA1d5doQRqF1DfHprziqgBLoCmI5aPGfM6PCOba2W84eSxdvAqClIufEjv 8 | UB3SgLmxAgMBAAECggEADmsAjH8GjWnUoYTWyXgFkClTsQeKB3aSwUebR5YcjZdm 9 | D5vMjkLEPieXwXUXm17sMh5p59yhrFhZDll7qBbgz97V7mFzFMVtuxn1SPudpzpH 10 | hfbQRWRuTH6vP6S/L6BuG6SYMw2D6Zs00cxUWPKqZSbpZ80t8osVRpTjxucDcMAF 11 | JcRoNX+325YlSHEEjJE+zbPmlNX0rAXMdyRs+5FiTkINtRl1o6xDgRwXrk7M8PBv 12 | wUUJedHK/4uU6qytIEues/yJWkLdpnu0mgkzcEMQMXD69TT28eLvmOv2NCwFAemp 13 | QN3SbB1NaF6LzsT0wWLDoE/A0FIHQzs3z97IbsAs+wKBgQD0B4UV8/BflILHV87l 14 | eA0uFrletAeA+sg0kmAjV3rVhy8IsRUnuUgPtWHSvv6QQUgrFKnH+XWCDvm8YlMB 15 | afpaPz69ZydTF7d1l3Sw99bREJE8uQIShMLAPmLuLD3Q0Pk0br2yzqcpvrShlCG6 16 | ia1y6yEpLTJA0mZ7NbgxHCDmlwKBgQDzSNR3iFC+A4q2WVJ6j7GFYkg/HjsAPjJ6 17 | 9MIgNizDmvBEtNF8Dqyq4XU7KxtMK9j7SiimsTKkeF4G6rh6AKaf99NPjuWZhm/7 18 | Q/RNFs4FlNb/8FXtWZq3JsbZyaRp/o/jSfWgRsu4oiQhqL5nqv0XM2pIib/vmpIU 19 | BqXyteVy9wKBgF8M+sqlPLCOES6CRkVdMI0OLt/zcaTMieToSugZL/Ax+qEBEMNr 20 | SOVNei/zUwZvVyPopYUN5rZlDONSzRAU7n3ueoqdvlSAPWZhOwOfVZ4TPO8RBPyf 21 | l5f39OLeeql2bEr/A4a9NaFt9b+mCkk1TUkgysbWIufazC4bq4X9ddc7AoGATu+C 22 | gIYqLHzZtPCmYj3dS3noFxKn8hw8JMjlc64gOBc9fg1tKuNYAtnEP75szPotHNui 23 | 9PLpi5PCblwaHvu3FJBEb7vdo0KLcutJiPmtPwJcAA7q0mgQWvyp6GAUiI+gAA8v 24 | MyHFV9LEBmfJ37kLBUwZYA/RxtxQKU8+6NE78WECgYEAoq20TDRdKj8UgssJ8T35 25 | DYpNG+Twd2gPUAYFcNkoElf4B1U15Btn33zOOL2lilnOe/64krlQHSmk07TyjsMX 26 | cgVWX8QLjaE+3nNs5wjLOpBDXc5FAs0+kKiz0YtIiaBusdZ/IT1twTes02uCZrje 27 | ChOHTYv1OiE000JehcAH0L0= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/19a_dns_invalid_ipv6.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/ipv6.sh" 2 | 3 | script_type="up" 4 | 5 | # busctl should not be called for any test in here 6 | TEST_BUSCTL_CALLED=0 7 | 8 | cases=( 9 | # More than one \`::' 10 | 1234::567::89:ab 11 | 1:::8 12 | 13 | # Too long 14 | 1234:567:89:a:b:c:d:e:f 15 | 16 | # Also too long 17 | 1234:567:89:0::c:d:e:f 18 | 19 | # Leading colon 20 | :1234:567:89:a:b:c:d:e 21 | 22 | # Trailing colon 23 | 1234:567:89:a:b:c:d:e: 24 | 25 | # Not hexadecimal 26 | ::zzzz 27 | 28 | # Bad embedded IPv4 29 | ::ffff:999.999.999.999 30 | 31 | # Embedded IPv4 in wrong location 32 | 1.2.3.4::ffff 33 | 34 | # Leading garbage 35 | @--@::1 36 | 37 | # Plain garbage 38 | : 39 | ::: 40 | ) 41 | 42 | all_ipv6_addresses_invalid() { 43 | local -a improperly_accepted_ipv6=() 44 | local -a wrongly_parsed_ipv6=() 45 | local ipv6 bad status 46 | 47 | for ipv6 in "${cases[@]}"; do 48 | foreign_option_1="dhcp-option DNS ${ipv6}" 49 | 50 | if source update-systemd-resolved; then 51 | improperly_accepted_ipv6+=("$ipv6") 52 | fi 53 | 54 | if ! bad="$(all_ipv6_expansion_implementations "$ipv6")"; then 55 | wrongly_parsed_ipv6+=("$bad") 56 | fi 57 | done 58 | 59 | if ((${#improperly_accepted_ipv6[@]} > 0)); then 60 | printf 1>&2 -- 'improperly accepted the following IPv6 addresses:\n' 61 | printf 1>&2 -- ' %s\n' "${improperly_accepted_ipv6[@]}" 62 | status=1 63 | fi 64 | 65 | if ((${#wrongly_parsed_ipv6[@]} > 0)); then 66 | printf 1>&2 -- 'parse for the following IPv6 addresses wrongly succeeded:\n' 67 | 68 | for bad in "${wrongly_parsed_ipv6[@]}"; do 69 | printf 1>&2 -- ' %s\n' "$bad" 70 | done 71 | 72 | status=1 73 | fi 74 | 75 | return "${status:-0}" 76 | } 77 | 78 | TEST_TITLE="Known-bad IPv6 addresses are all rejected" 79 | checktest all_ipv6_addresses_invalid 80 | -------------------------------------------------------------------------------- /tests/24_custom_foreign_options.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/foreign_options.sh" 2 | 3 | script_type="up" 4 | 5 | test_valid_custom_foreign_options() { 6 | local test_option test_value 7 | 8 | for test_option in FLUSH-CACHES RESET-STATISTICS RESET-SERVER-FEATURES; do 9 | for test_value in true false yes no; do 10 | run_custom_foreign_option_test "$test_option" "$test_value" 11 | run_custom_foreign_option_test "$test_option" "${test_value^}" 12 | done 13 | done 14 | 15 | for test_option in DNS-OVER-TLS DEFAULT-ROUTE LLMNR MULTICAST-DNS; do 16 | for test_value in true false yes no; do 17 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" "$test_value" "$test_value" 18 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" "${test_value^}" "$test_value" 19 | done 20 | done 21 | 22 | for test_option in DNS-OVER-TLS LLMNR MULTICAST-DNS; do 23 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" default default 24 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" Default default 25 | done 26 | 27 | for test_option in LLMNR MULTICAST-DNS; do 28 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" resolve resolve 29 | done 30 | 31 | run_custom_foreign_option_test_with_ip_ifindex DNS-OVER-TLS opportunistic opportunistic 32 | } 33 | 34 | test_invalid_custom_foreign_options() { 35 | local test_option test_value 36 | 37 | EXPECT_FAILURE=1 38 | 39 | for test_option in FLUSH-CACHES RESET-STATISTICS RESET-SERVER-FEATURES; do 40 | for test_value in "" nope yessirree; do 41 | run_custom_foreign_option_test "$test_option" "$test_value" 42 | done 43 | done 44 | 45 | for test_option in DNS-OVER-TLS DEFAULT-ROUTE LLMNR MULTICAST-DNS; do 46 | for test_value in "" nope yessirree; do 47 | run_custom_foreign_option_test_with_ip_ifindex "$test_option" "$test_value" "$test_value" 48 | done 49 | done 50 | } 51 | 52 | test_valid_custom_foreign_options 53 | test_invalid_custom_foreign_options 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug Report' 2 | description: 'File a bug report' 3 | title: '[Bug]: ' 4 | labels: 5 | - Bug 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: 'Thank you for reporting this bug.' 10 | - type: input 11 | id: version 12 | attributes: 13 | label: 'Version in use.' 14 | description: 'What version of `update-systemd-resolved` are you using?' 15 | placeholder: '1.2.0, master, 8d184d8' 16 | - type: input 17 | id: distribution 18 | attributes: 19 | label: 'Your Linux distribution.' 20 | description: >- 21 | What is the name and, if applicable, release of the Linux distro on the affected machine? 22 | placeholder: 'Ubuntu 22.04.3 LTS, Arch Linux, NixOS 23.05' 23 | - type: input 24 | id: systemd-version 25 | attributes: 26 | label: 'Your systemd version.' 27 | description: 'What version of systemd are you running on the affected machine?' 28 | placeholder: 'First line of the output of `systemctl --version`' 29 | - type: dropdown 30 | id: network-managment 31 | attributes: 32 | label: 'Your network management software.' 33 | description: 'What are you using to manage networking on the affected machine?' 34 | options: 35 | - 'systemd-networkd' 36 | - 'NetworkManager' 37 | - 'Something else (please give details in the bug description text field)' 38 | default: 2 39 | - type: textarea 40 | id: description 41 | attributes: 42 | label: 'Please describe the bug.' 43 | description: 'What happened, and what did you expect to happen?' 44 | placeholder: 'What went wrong?' 45 | - type: textarea 46 | id: resolvectl-status 47 | attributes: 48 | label: 'Output of `resolvectl status`.' 49 | description: >- 50 | Please run `resolvectl status` on the affected machine and post the 51 | output here, redacting any sensitive information. 52 | - type: textarea 53 | id: miscellaneous 54 | attributes: 55 | label: 'Other helpful details.' 56 | description: >- 57 | Please share anything else that may help in diagnosing this bug. 58 | placeholder: 'For instance, `journalctl -xeu my-openvpn-client.service`' 59 | -------------------------------------------------------------------------------- /nix/rootCA-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDMNSbyBc0XUyMP 3 | JPHVERp+zc8vJHFZDWe2O0NUzK24vodhqREs1+xy3QgGIdCTVWXc9fXFOAX6TAck 4 | hWkP4j3riPveiCCjnsKggbHvoDSlNqNVcGAVnOnGJtM1R/+85HYfql7iejx5etTm 5 | 5irfwCpvw49I3XvaeVWg3siASxvlxt39+bz9JIin377YsP9c9bxzlT2lqHWL+uXV 6 | 6XPBvzrjKC2bWv7BiaZqm+kPvxsUxqqT+PW9oqCWay6w6shLnvICRwke0YNPi2I9 7 | Yp71I2QMTv/IuXjUOpyi8dZLRYB2WT0gOvndBCYNhMnbE8mmJoZinZMx4CCO7sPQ 8 | 9pURlHXvTtvTEyTWdHE9zuweph45nJSrei76uUHylEQrqoAnSKkWyfBBKxS0YNbn 9 | IWImodsIfexIpSuHhFohqZnUps8KKKe4+Uatqk7VKTCus4alzIQ4Vs1Gb1JN3GYd 10 | qq3kvOtLPiQe67WzHQJ91jXE9HDrQx+EqTzfAeOdoeVe64Eq8qkCAwEAAQKCAYEA 11 | seGbCzgCb078OzTzc6ZybgLZdzdHhUsoDJWTEUs6CLPvOiML0wRD88qWMsFB7xV0 12 | pgWbETC8BEw17JpJ6owpZALvY+kwhVbGMwrG9PWY5lGx9brt9+W3veQUF1Wgb+qS 13 | +wJtpNrV0vwsePYGYuICFVlEdzR3rtgCvx9RiG/k3UNeHN5uwhQQ9irxE9EaoN9u 14 | SUC3cpZLzqO/kZbKPvtVUIqvL6UURYKidDtbyVuvO2nTLRKw/X+sY1r6USIzV6wb 15 | k5xOSsu3Rxc2fEebQ7agWdlOKPYRZ7oJUJTyDFx3I9NkhmBowseQRcANkmBhEL1k 16 | XkniCsS96TwCeSx+K2bTkDlYRI3G12u2dv0kvW/x8FsXF2nLkCsdwL52V5VgTVgh 17 | 0eDs54FxA9N160xkSay264cDySSWMJKQRr/VqZyt39oI2YDmer2pl0O1e/QTtMo5 18 | eXbHryKyKSDmZV5Ei6ZQC4oWE5/8RGgNx4ch961yrHkQvC7RPL6DsXMSwV9I5iwB 19 | AoHBAM85OKi0kDA3/BCUxWF5//7gpEXpVoY943XiszDEwvcPYSRnaR44Ny/QRnZR 20 | Qha4TcA/rTHkm66CmhnWLYi51EKjERD2Zonkq8FSXdZpsjQ3SqayjC8MszPLZukj 21 | 4uxosLpMFioktlxG/f2B+2jyai63B3KRTAYqpldsrCEFrudFQsuZzWNbl0vs9QHP 22 | oQ/O6LgJVcA5CgE6P+1XVEQMEDTC+X6u4ATlKw4Pvxk9gYL6iRnfEUmeAPlV/3vA 23 | mBUKnwKBwQD8RjNMq0pI4Ams0ApQtiTW7i2U3/GrNZ2TqvbBwZXj+gmAG/bCBhuT 24 | 38NPmmgOuFCeJDX6pIU+lZTQK+xObZPwWBhBp5kcHqIHvCT6p4u7lFKwZjAGQ0+y 25 | UTolxhOaenMByelDTSV+00ZDcSAKhhxdhGLBO5/8AHFhaZKJyg+Bj7IvVpO2oTVu 26 | 6mHY5rJRQ7a/zJnLMBswIqdPA2CdV3ncGpXUOnereOhxZ0guzWr8ds5t8t0J1vfz 27 | jHQAgPaMxbcCgcB/tQ8TAXxfCxGgEl92TF6U8FKs9zmor5lvvE+cfZZ99g9zBPwG 28 | cLSqFdxm7HsjT2AzW8rcFbxQFxLrW1Bik8uZaa+J2aCl2LR1BtLn4em+PlkWVLEK 29 | CfSitfbtNX2THo3TsjJytH9ibSn4wtNzAPqpYYkIdTz6C+zJsiJ+k2cQBmI84cNv 30 | OTILy7PO8uuat3Q6fx5GwaBF02U0Wv6GlTyjl4l1JkbPHYCkQNYPsxUO6GH3/L5F 31 | tUd6YiJ6XN4dEZcCgcBo+q2OUhlvigt8pnYkcCeUaTj+otJmdMFGGfblWjGN1Rbv 32 | ALQGuZPwTUVxcseqmHiz1k3AJ4ZrLMPofN6xJFhTw9UUPTIxyW2T2m9o/x/exzJB 33 | xcRmVsxrX/HaljrCJgKF1AgFwazAwhqTJhg3SOe04spVrwI8U9LavpwEStl5CNsV 34 | Z+nALgWWSmK9aAL8XjlGR1YYf8RQm5sT/kvOLgC/3zBKSKpT6NSRnHElSMYkmSv+ 35 | BPqGhbZY2zHKo9/1ZLECgcAoRB8yme7dGLFDCoGTlB5JZscPkjYlxIvKAs4dtOhQ 36 | 4DMIKTx11bLu0sii13HcmcfrL80XdSfmBBo1nDBq8ghUuw2qitaT7fgW9v7KLD10 37 | eHQiOQVrS75batgbCp2dZzFWLdxBGBy5Te8IWS82bBKoXF0Shx4qhMho6FiLGVOL 38 | qVhtZr0FZT4qKDUsTA/2XKa6GVfE5zvGd/YNZMqupmFSOBtMHv7uj2QlvTlW6E6V 39 | 1TWW3HAmp7W2+2HX9u4uJIE= 40 | -----END PRIVATE KEY----- 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Similar to the directory variables specified here: 2 | # https://www.gnu.org/prep/standards/html_node/Directory-Variables.html 3 | PREFIX ?= /usr/local 4 | EXEC_PREFIX ?= $(PREFIX) 5 | LIBEXECDIR ?= $(EXEC_PREFIX)/libexec 6 | DATAROOTDIR ?= $(PREFIX)/share 7 | DATADIR ?= $(DATAROOTDIR) 8 | 9 | SRC = update-systemd-resolved 10 | DEST = $(DESTDIR)/$(LIBEXECDIR)/openvpn/$(SRC) 11 | CONF = $(DESTDIR)/$(DATADIR)/doc/openvpn/$(SRC).conf 12 | RULES = $(DESTDIR)/$(DATADIR)/polkit-1/rules.d/10-$(SRC).rules 13 | RULES_OPTIONS ?= --polkit-allowed-user=openvpn --polkit-allowed-group=network 14 | 15 | .PHONY: all install info rules clean force-clean test nixos-test nixos-test-driver 16 | 17 | all: install info 18 | 19 | $(DEST): $(SRC) 20 | @install -Dm750 $< $@ 21 | 22 | $(CONF): $(SRC).conf 23 | @install -Dm644 $< $@ 24 | 25 | $(RULES): $(SRC) 26 | @mkdir -p $$(dirname $@) 27 | @./$(SRC) print-polkit-rules $(RULES_OPTIONS) > $@ 28 | 29 | install: $(DEST) $(CONF) $(RULES) 30 | 31 | rules: $(RULES) 32 | 33 | clean: 34 | @rm -i $(DEST) $(CONF) $(RULES) 35 | 36 | force-clean: 37 | @rm -f $(DEST) $(CONF) $(RULES) 38 | 39 | info: 40 | @printf 'Successfully installed %s to %s.\n' $(SRC) $(DEST) 41 | @echo 42 | @echo 'Now would be a good time to update /etc/nsswitch.conf:' 43 | @echo 44 | @echo ' # Use systemd-resolved first, then fall back to /etc/resolv.conf' 45 | @echo ' hosts: files resolve dns myhostname' 46 | @echo ' # Use /etc/resolv.conf first, then fall back to systemd-resolved' 47 | @echo ' hosts: files dns resolve myhostname' 48 | @echo 49 | @echo 'You should also update your OpenVPN configuration:' 50 | @echo 51 | @echo ' script-security 2' 52 | @printf ' up %s\n' $(DEST) 53 | @echo ' up-restart' 54 | @printf ' down %s\n' $(DEST) 55 | @echo ' down-pre' 56 | @echo 57 | @echo ' # If needed, to permit `update-systemd-resolved` to find utilities it depends' 58 | @echo ' # on. Adjust to suit your system.' 59 | @echo ' #setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' 60 | @echo 61 | @printf 'or pass --config %s\n' $(CONF) 62 | @echo 'in addition to any other --config arguments to your openvpn command.' 63 | @echo 64 | @printf 'Please also consider putting the polkit rules %s in /etc/polkit-1/rules.d.\n' $(RULES) 65 | 66 | test: 67 | @./run-tests 68 | 69 | nixos-test: 70 | @nix build -L ".#checks.$$(nix eval --impure --raw --expr builtins.currentSystem).update-systemd-resolved" 71 | 72 | # Enter a console with NixOS test machines available 73 | nixos-test-driver: 74 | @$$(nix-build --no-out-link -A update-systemd-resolved.nixosTest.driver ./nix)/bin/nixos-test-driver --keep-vm-state 75 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-request.yml: -------------------------------------------------------------------------------- 1 | name: 'Support Request' 2 | description: 'Request support' 3 | title: '[Help]: ' 4 | labels: 5 | - 'Help Wanted' 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: 'Thank you for reporting this issue.' 10 | - type: input 11 | id: version 12 | attributes: 13 | label: 'Version in use.' 14 | description: 'What version of `update-systemd-resolved` are you using?' 15 | placeholder: '1.2.0, master, 8d184d8' 16 | - type: input 17 | id: distribution 18 | attributes: 19 | label: 'Your Linux distribution.' 20 | description: >- 21 | What is the name and, if applicable, release of the Linux distro on the affected machine? 22 | placeholder: 'Ubuntu 22.04.3 LTS, Arch Linux, NixOS 23.05' 23 | - type: input 24 | id: systemd-version 25 | attributes: 26 | label: 'Your systemd version.' 27 | description: 'What version of systemd are you running on the affected machine?' 28 | placeholder: 'First line of the output of `systemctl --version`' 29 | - type: dropdown 30 | id: network-managment 31 | attributes: 32 | label: 'Your network management software.' 33 | description: 'What are you using to manage networking on the affected machine?' 34 | options: 35 | - 'systemd-networkd' 36 | - 'NetworkManager' 37 | - 'Something else (please give details in the issue description text field)' 38 | default: 2 39 | - type: textarea 40 | id: description 41 | attributes: 42 | label: 'Please describe the issue.' 43 | description: 'What happened, and what did you expect to happen?' 44 | placeholder: 'What went wrong?' 45 | - type: textarea 46 | id: resolvectl-status 47 | attributes: 48 | label: 'Output of `resolvectl status`.' 49 | description: >- 50 | Please run `resolvectl status` on the affected machine and post the 51 | output here, redacting any sensitive information. 52 | - type: textarea 53 | id: miscellaneous 54 | attributes: 55 | label: 'Other helpful details.' 56 | description: >- 57 | Please share anything else that may help in diagnosing this issue. 58 | placeholder: 'For instance, `journalctl -xeu my-openvpn-client.service`' 59 | - type: checkboxes 60 | id: averrals 61 | attributes: 62 | label: 'I have read and followed relevant documentation.' 63 | options: 64 | - label: >- 65 | (If reporting DNS leakage) I have read [the "DNS Leakage" README 66 | section](https://github.com/jonathanio/update-systemd-resolved#dns-leakage). 67 | - label: >- 68 | (If you are using NetworkManager) I have read the [known issues 69 | with NetworkManager](https://github.com/jonathanio/update-systemd-resolved#networkmanager). 70 | -------------------------------------------------------------------------------- /tests/19b_dns_valid_ipv6.sh: -------------------------------------------------------------------------------- 1 | source "${BASH_SOURCE[0]%/*}/helpers/ipv6.sh" 2 | 3 | script_type="up" 4 | 5 | TEST_BUSCTL_CALLED=1 6 | TEST_BUSCTL_DNS=SKIP 7 | 8 | # update-systemd-resolved should exit nonzero for all tests 9 | EXPECT_FAILURE=0 10 | 11 | private=( 12 | fc00:: 13 | fc01::1234 14 | fdef:: 15 | fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 16 | ) 17 | 18 | public=( 19 | ::abcd:1234 20 | 1:: 21 | 2:: 22 | 1:1:1:1:: 23 | 2001:abcd:: 24 | abcd:: 25 | ) 26 | 27 | loopback=( 28 | ::1 29 | ) 30 | 31 | ipv4_mapped=( 32 | ::ffff:0:0 33 | ::ffff:0:1234 34 | ::ffff:1.2.3.4 35 | ::ffff:ffff:ffff 36 | ) 37 | 38 | discard=( 39 | 100:: 40 | 100::1234 41 | 100:0000:0000:0000:ffff:ffff:ffff:ffff 42 | ) 43 | 44 | multicast=( 45 | ff00:: 46 | ffff:: 47 | ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 48 | ) 49 | 50 | linklocal=( 51 | fe80:: 52 | fe89:: 53 | febf:: 54 | febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff 55 | ) 56 | 57 | teredo=( 58 | 2001:: 59 | 2001::1234 60 | 2001:0:ffff:ffff:ffff:ffff:ffff:ffff 61 | ) 62 | 63 | orchid=( 64 | 2001:10:: 65 | 2001:10::1234 66 | 2001:001F:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF 67 | ) 68 | 69 | documentation=( 70 | 2001:db8:: 71 | 2001:db8::1234 72 | 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff 73 | ) 74 | 75 | other=( 76 | fd00:ff:0:2:301:64::3 77 | 78 | # omitted; sipcalc does not handle these 79 | #1:2:3:4:5:6:7:: 80 | #::1:2:3:4:5:6:7 81 | ) 82 | 83 | cases=( 84 | "${private[@]}" 85 | "${public[@]}" 86 | "${loopback[@]}" 87 | "${ipv4_mapped[@]}" 88 | "${discard[@]}" 89 | "${multicast[@]}" 90 | "${linklocal[@]}" 91 | "${teredo[@]}" 92 | "${orchid[@]}" 93 | "${documentation[@]}" 94 | "${other[@]}" 95 | ) 96 | 97 | all_ipv6_addresses_valid() { 98 | local -a improperly_rejected_ipv6=() 99 | local -a wrongly_parsed_ipv6=() 100 | local ipv6 bad status 101 | 102 | for ipv6 in "${cases[@]}"; do 103 | foreign_option_1="dhcp-option DNS ${ipv6}" 104 | { 105 | if ! source update-systemd-resolved; then 106 | improperly_rejected_ipv6+=("$ipv6") 107 | fi 108 | 109 | if ! bad="$(all_ipv6_expansion_implementations "$ipv6")"; then 110 | wrongly_parsed_ipv6+=("$bad") 111 | fi 112 | } 1> /dev/null 113 | done 114 | 115 | if ((${#improperly_rejected_ipv6[@]} > 0)); then 116 | printf 1>&2 -- 'Improperly rejected the following IPv6 addresses:\n' 117 | printf 1>&2 -- ' %s\n' "${improperly_rejected_ipv6[@]}" 118 | status=1 119 | fi 120 | 121 | if ((${#wrongly_parsed_ipv6[@]} > 0)); then 122 | printf 1>&2 -- 'Parse for the following IPv6 addresses failed:\n' 123 | 124 | for bad in "${wrongly_parsed_ipv6[@]}"; do 125 | printf 1>&2 -- ' %s\n' "$bad" 126 | done 127 | 128 | status=1 129 | fi 130 | 131 | return "${status:-0}" 132 | } 133 | 134 | TEST_TITLE="Known-good IPv6 addresses are all accepted" 135 | checktest all_ipv6_addresses_valid 136 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devshell": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1741473158, 11 | "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", 12 | "owner": "numtide", 13 | "repo": "devshell", 14 | "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "numtide", 19 | "repo": "devshell", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-parts": { 24 | "inputs": { 25 | "nixpkgs-lib": "nixpkgs-lib" 26 | }, 27 | "locked": { 28 | "lastModified": 1741352980, 29 | "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=", 30 | "owner": "hercules-ci", 31 | "repo": "flake-parts", 32 | "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "hercules-ci", 37 | "repo": "flake-parts", 38 | "type": "github" 39 | } 40 | }, 41 | "nixpkgs": { 42 | "locked": { 43 | "lastModified": 1742422364, 44 | "narHash": "sha256-mNqIplmEohk5jRkqYqG19GA8MbQ/D4gQSK0Mu4LvfRQ=", 45 | "owner": "NixOS", 46 | "repo": "nixpkgs", 47 | "rev": "a84ebe20c6bc2ecbcfb000a50776219f48d134cc", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "NixOS", 52 | "ref": "nixos-unstable", 53 | "repo": "nixpkgs", 54 | "type": "github" 55 | } 56 | }, 57 | "nixpkgs-lib": { 58 | "locked": { 59 | "lastModified": 1740877520, 60 | "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=", 61 | "owner": "nix-community", 62 | "repo": "nixpkgs.lib", 63 | "rev": "147dee35aab2193b174e4c0868bd80ead5ce755c", 64 | "type": "github" 65 | }, 66 | "original": { 67 | "owner": "nix-community", 68 | "repo": "nixpkgs.lib", 69 | "type": "github" 70 | } 71 | }, 72 | "root": { 73 | "inputs": { 74 | "devshell": "devshell", 75 | "flake-parts": "flake-parts", 76 | "nixpkgs": "nixpkgs", 77 | "treefmt-nix": "treefmt-nix" 78 | } 79 | }, 80 | "treefmt-nix": { 81 | "inputs": { 82 | "nixpkgs": [ 83 | "nixpkgs" 84 | ] 85 | }, 86 | "locked": { 87 | "lastModified": 1742370146, 88 | "narHash": "sha256-XRE8hL4vKIQyVMDXykFh4ceo3KSpuJF3ts8GKwh5bIU=", 89 | "owner": "numtide", 90 | "repo": "treefmt-nix", 91 | "rev": "adc195eef5da3606891cedf80c0d9ce2d3190808", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "numtide", 96 | "repo": "treefmt-nix", 97 | "type": "github" 98 | } 99 | } 100 | }, 101 | "root": "root", 102 | "version": 7 103 | } 104 | -------------------------------------------------------------------------------- /nix/devshells.nix: -------------------------------------------------------------------------------- 1 | {lib, ...}: { 2 | perSystem = { 3 | config, 4 | pkgs, 5 | system, 6 | ... 7 | }: { 8 | devshells.default = { 9 | commands = [ 10 | { 11 | name = "fmt"; 12 | category = "linting"; 13 | help = "Format this project's code"; 14 | command = '' 15 | exec ${config.treefmt.build.wrapper}/bin/treefmt "$@" 16 | ''; 17 | } 18 | 19 | { 20 | name = "mkdotcert"; 21 | category = "maintenance"; 22 | help = "Generate the DNS-over-TLS keypair for use in system testing"; 23 | command = let 24 | inherit (config.checks.update-systemd-resolved.nodes) resolver; 25 | in '' 26 | export CAROOT="''${PRJ_ROOT:-.}/nix" 27 | ${pkgs.mkcert}/bin/mkcert -install || exit 28 | ${pkgs.mkcert}/bin/mkcert \ 29 | -cert-file "''${CAROOT}/resolver.crt" \ 30 | -key-file "''${CAROOT}/resolver.key" \ 31 | ${resolver.networking.hostName} \ 32 | ${resolver.networking.hostName}.${resolver.networking.domain} 33 | ''; 34 | } 35 | 36 | { 37 | name = "mkanchor"; 38 | category = "maintenance"; 39 | help = "Fetch DNSSEC root anchors and translate them to dnsmasq format"; 40 | command = let 41 | unsupported = lib.elem system [ 42 | "armv6l-linux" 43 | "armv7l-linux" 44 | "powerpc64le-linux" 45 | "riscv64-linux" 46 | ]; 47 | in 48 | (lib.optionalString (!unsupported) '' 49 | ${pkgs.xidel}/bin/xidel \ 50 | --input-format xml \ 51 | --output-format json-wrapped \ 52 | -e 'for $kd in //TrustAnchor/KeyDigest return string-join((//TrustAnchor/Zone, $kd/KeyTag, $kd/Algorithm, $kd/DigestType, $kd/Digest), ",")' \ 53 | https://data.iana.org/root-anchors/root-anchors.xml \ 54 | | ${pkgs.jq}/bin/jq flatten > "''${PRJ_ROOT}/nix/trust-anchor.json" 55 | '') 56 | + (lib.optionalString unsupported '' 57 | printf 1>&2 -- '%s: sorry, this command is unsupported on system `%s`\n' \ 58 | "''${0##*/}" ${lib.escapeShellArg system} 59 | exit 1 60 | ''); 61 | } 62 | 63 | { 64 | name = "mkoptdocs"; 65 | category = "maintenance"; 66 | help = "Generate NixOS module options documentation"; 67 | command = '' 68 | docs="$(${pkgs.nix}/bin/nix "$@" build --print-out-paths --no-link "''${PRJ_ROOT}#docs")" || exit 69 | 70 | seen=0 71 | while read -r path; do 72 | seen="$((seen + 1))" 73 | if [ "$seen" -gt 1 ]; then 74 | printf 1>&2 -- 'error: more than one output path...\n' 75 | exit 1 76 | fi 77 | install -Dm0644 "$path" "''${PRJ_ROOT}/docs/nixos-modules.md" 78 | done </update-systemd-resolved.conf" from within an 76 | # OpenVPN config file will work properly). 77 | postInstall = '' 78 | sed -i -e " 79 | s|\([[:space:]]\)[^[[:space:]]*\(/update-systemd-resolved\)|\1''${out}/libexec/openvpn\2| 80 | " "''${out}/share/doc/openvpn/update-systemd-resolved.conf" 81 | 82 | wrapProgram "''${out}/libexec/openvpn/update-systemd-resolved" \ 83 | --suffix PATH : ${lib.makeBinPath buildInputs} 84 | ''; 85 | 86 | patches = []; 87 | 88 | # Rewrite the test script's shebang to use a Bash that lives somewhere 89 | # in the Nix store, rather than using `#!/usr/bin/env bash`. Without 90 | # this, the test suite will fail with an error like: 91 | # 92 | # /nix/store/<...>/bin/bash: line 1: ./run-tests: cannot execute: required file not found 93 | # 94 | # Additionally, update the shebang of `update-systemd-resolved` itself in 95 | # order to permit running the `--print-polkit-rules` action. 96 | patchPhase = '' 97 | patchShebangs ./run-tests ./update-systemd-resolved 98 | ''; 99 | 100 | passthru = { 101 | # Note that we write the rules to a file with the extension ".js"; 102 | # "node --check" bails out if provided a file that ends with the 103 | # ".rules" extension. 104 | mkPolkitRules = { 105 | user, 106 | group, 107 | rules ? "10-update-systemd-resolved.rules", 108 | }: 109 | pkgs.runCommand rules {} 110 | '' 111 | case "$out" in 112 | *.rules) 113 | rules="$out" 114 | ;; 115 | *) 116 | rules="''${out}.rules" 117 | esac 118 | 119 | case "$rules" in 120 | */*) 121 | mkdir -p "$(dirname "$rules")" 122 | ;; 123 | esac 124 | 125 | js="''${rules}.js" 126 | 127 | ${config.packages.update-systemd-resolved}/libexec/openvpn/update-systemd-resolved print-polkit-rules \ 128 | --polkit-allowed-user ${user} \ 129 | --polkit-allowed-group ${group} \ 130 | > "$js" 131 | 132 | ${pkgs.nodejs}/bin/node --check "$js" 133 | mv -f "$js" "$rules" 134 | ''; 135 | 136 | tests = { 137 | inherit (config.checks) update-systemd-resolved; 138 | 139 | polkit-rules = config.packages.update-systemd-resolved.mkPolkitRules { 140 | user = "foo"; 141 | group = "bar"; 142 | }; 143 | }; 144 | }; 145 | }); 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking on update-systemd-resolved 2 | 3 | ## Nix flake usage 4 | 5 | This project supplies a [`flake.nix`](./flake.nix) file defining a Nix 6 | flake.[^nix-flakes] 7 | 8 | [^nix-flakes]: See the [NixOS wiki](https://nixos.wiki/wiki/Flakes) and the 9 | [`nix flake` page in the Nix package manager reference manual](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html) 10 | for background on Nix flakes. 11 | 12 | This Nix flake defines three important important outputs: 13 | 14 | 1. A Nix package for `update-systemd-resolved` (see [here](./nix/packages.nix)), 15 | 2. A NixOS test of `update-systemd-resolved` features (see 16 | [here](./nix/checks.nix)), and 17 | 3. A Nix development shell[^devshell] (see [here](./nix/devshells.nix)). 18 | 19 | [^devshell]: Based on the [`numtide/devshell`](https://github.com/numtide/devshell) project. 20 | 21 | In order to work on the `update-systemd-resolved` project's Nix features, 22 | you'll need to [install the Nix package 23 | manager](https://nixos.org/download.html) and [ensure that the `flakes` and 24 | `nix-command` experimental features are 25 | enabled](https://nixos.wiki/wiki/Flakes#Enable_flakes). 26 | 27 | ### Building the `update-systemd-resolved` package 28 | 29 | To build the `update-systemd-resolved` package exposed by this flake, run the 30 | following command:[^verbose-output] 31 | 32 | [^verbose-output]: Note that the `-L` flag can be omitted for terser output. 33 | 34 | ```shell-session 35 | $ nix build -L '.#' 36 | ``` 37 | 38 | Or: 39 | 40 | ```shell-session 41 | $ nix build -L '.#update-systemd-resolved' 42 | ``` 43 | 44 | These two forms are functionally equivalent because the 45 | `update-systemd-resolved` package is the default package. 46 | 47 | In addition to building the package, `nix build` will place a symbolic link to 48 | its output path at `./result` (`ls -lAR ./result/`, `tree ./result/`, or 49 | similar to see what the package contains). 50 | 51 | ### Nix flake checks 52 | 53 | This project includes a NixOS test of `update-systemd-resolved`'s functionality 54 | and features, exposed as a Nix flake check. 55 | 56 | Please see the NixOS manual's ["Testing with NixOS" section](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests) 57 | for information on [writing](https://nixos.org/manual/nixos/stable/index.html#sec-writing-nixos-tests) 58 | and [calling](https://nixos.org/manual/nixos/stable/index.html#sec-calling-nixos-tests) 59 | NixOS tests, as well as information on [running them interactively](https://nixos.org/manual/nixos/stable/index.html#sec-running-nixos-tests-interactively) 60 | and [linking them to Nix packages](https://nixos.org/manual/nixos/stable/index.html#sec-linking-nixos-tests-to-packages). 61 | 62 | #### How the test works 63 | 64 | This project's NixOS test sets up three machines: 65 | 66 | 1. An OpenVPN server, 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. 73 | 74 | The OpenVPN server and client run a [point-to-point configuration with a static 75 | key](https://openvpn.net/community-resources/static-key-mini-howto/). The 76 | client's OpenVPN configuration file includes several instances of [the 77 | `dhcp-option`s recognized by `update-systemd-resolved`](./README.md#usage). 78 | The [test script](https://nixos.org/manual/nixos/stable/index.html#test-opt-testScript) 79 | starts all three machines, waits for certain systemd services to become active, 80 | and then asserts that certain hostnames are resolvable _from the client_ that 81 | would not be resolvable unless the client were configured to use the DNS 82 | settings specified in its OpenVPN configuration file. 83 | 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. Dnsmasq handles DNSSEC (though 87 | we exempt the VPN domain from DNSSEC validation, as a test of the 88 | `SetLinkDNSSECNegativeTrustAnchors` feature). 89 | 90 | #### Extending the NixOS test 91 | 92 | If you are implementing a new feature or correcting a bug in 93 | `update-systemd-resolved`, you are encouraged to extend the NixOS test with new 94 | test cases that exercise your feature or verify that the bug is fixed -- 95 | _unless_ you can adequately express these within the [unit test 96 | suite](./README.md#how-to-help), as that suite runs significanly faster than 97 | the NixOS test and has no prerequisites other than Bash. 98 | 99 | #### Running Nix flake checks 100 | 101 | To run the [above-mentioned](#nix-flake-usage) NixOS system test and other 102 | checks,[^other-checks] execute the following command:[^verbose-output] 103 | 104 | ```shell-session 105 | $ nix flake check -L 106 | ``` 107 | 108 | [^other-checks]: Other checks include linting the Nix source code in this 109 | project, and running a syntax check on the polkit rules 110 | generated by `update-systemd-resolved print-polkit-rules`. 111 | 112 | ##### Running a check for a specific system 113 | 114 | Running `nix flake check [-L]` will execute Nix flake checks for all supported 115 | systems.[^supported-systems] To run a check for a particular system, instead 116 | use the `nix build` command. For instance, to execute the NixOS system test 117 | for the `x86_64-linux` system, run: 118 | 119 | ```shell-session 120 | $ nix build -L '.#checks.x86_64-linux.update-systemd-resolved' 121 | ``` 122 | 123 | [^supported-systems]: Run `nix flake show` to view flake outputs namespaced by 124 | all supported systems. 125 | 126 | #### Maintaining NixOS test assets 127 | 128 | ##### Regenerating the DNS-over-TLS keypair 129 | 130 | To regenerate the keypair used for testing DNS-over-TLS, [enter the 131 | devshell](#entering-the-nix-development-shell) and [run 132 | `mkdotcert`](#summary-of-available-commands). 133 | 134 | ##### Regenerating the DNSSEC root anchors 135 | 136 | To regenerate the dnsmasq root anchor specification used for testing DNSSEC, 137 | [enter the devshell](#entering-the-nix-development-shell) and [run 138 | `mkanchor`](#summary-of-available-commands). 139 | 140 | ### Entering the Nix development shell 141 | 142 | To enter the Nix development shell, run the following command: 143 | 144 | ```shell-session 145 | $ nix develop 146 | ``` 147 | 148 | You will be presented with a menu of commands available within the development 149 | shell. 150 | 151 | #### Summary of available commands 152 | 153 | [`alejandra`]:https://github.com/kamadorueda/alejandra 154 | [`shellcheck`]: https://shellcheck.net 155 | [`shfmt`]: https://github.com/mvdan/sh 156 | 157 | - `fmt`: format all shell and Nix code in this project using, respectively, 158 | [`shfmt`][] and [`alejandra`][]. Additionally, lint shell code with 159 | [`shellcheck`][]. 160 | - `mkdotcert`: regenerate the keypair used for encrypting DNS-over-TLS in the 161 | NixOS system test. 162 | - `mkanchor`: regenerate the DNSSEC trust anchors configuration used with 163 | dnsmasq in the NixOS system test. 164 | - `mkoptdocs`: regenerate the [NixOS module options documentation](/docs/nixos-modules.md) 165 | -------------------------------------------------------------------------------- /run-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Test Framework for update-systemd-resolved. 4 | # Copyright (C) 2016, Jonathan Wright 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # Set colour escape sequences 17 | 18 | PS4='+ ${BASH_SOURCE:-$0}@${LINENO:-0}${FUNCNAME:+#${FUNCNAME}()}: ' 19 | 20 | if [[ -n ${NO_COLOR-} ]] || ! colors="$(tput colors 2> /dev/null)" || (("${colors:-0}" < 16)); then 21 | RED='' 22 | YELLOW='' 23 | ORANGE='' 24 | GREEN='' 25 | DARK='' 26 | RESET='' 27 | else 28 | RED='\033[0;31m' 29 | YELLOW='\033[1;33m' 30 | ORANGE='\033[0;33m' 31 | GREEN='\033[0;32m' 32 | DARK='\033[1;30m' 33 | RESET='\033[0m' 34 | fi 35 | 36 | # Set Pass/Fail signatures 37 | PASS="✓" 38 | FAIL="✗" 39 | 40 | # Counters 41 | COUNT_PASS=0 42 | COUNT_FAIL=0 43 | 44 | # Names of files in which test cases failed 45 | declare -A FAILED 46 | 47 | # Flag for determining whether a test script called the `runtest' function 48 | RUNTEST_CALLED=0 49 | 50 | AUTOMATED_TESTING=1 51 | 52 | if ! toplevel="$(git rev-parse --show-toplevel 2> /dev/null)"; then 53 | if ! toplevel="$(readlink -f "${BASH_SOURCE[0]%/*}/.." 2> /dev/null)"; then 54 | toplevel="${PWD:-$(pwd)}" 55 | fi 56 | fi 57 | 58 | # Ensure that things like "source update-systemd-resolved" pick up the script 59 | # in this repository's toplevel rather than, say, 60 | # /usr/bin/update-systemd-resolved. 61 | export PATH="${toplevel}${PATH:+:${PATH}}" 62 | unset toplevel 63 | 64 | busctl2var() { 65 | if (("$#" != 2)); then 66 | printf 1>&2 -- 'usage: %s VARIABLE_NAME BUSCTL_CALL\n' "${FUNCNAME[0]}" 67 | return 1 68 | fi 69 | 70 | local -n var_ref="$1" 71 | shift 72 | 73 | # Compatible with busybox; with GNU sed's `\U` extension we could all the 74 | # string-munging work with sed alone and not require `tr` in the pipeline. 75 | local varname_suffix 76 | varname_suffix="$( 77 | printf -- '%s' "$1" | 78 | sed -e ' 79 | s/[^[:alnum:]]//g 80 | s/\([a-z]\)\([A-Z]\)/\1_\2/g 81 | s/\([A-Z]\)\([A-Z][a-z]\)/\1_\2/g 82 | ' | 83 | tr '[:lower:]' '[:upper:]' | 84 | sed 's/^SET_LINK_//' 85 | )" 86 | 87 | # `var_ref` is a reference variable; we use it by assigning to it. 88 | # shellcheck disable=SC2034 89 | var_ref="TEST_BUSCTL_${varname_suffix}" 90 | } 91 | 92 | busctl() { 93 | # Short-circuit if we got "busctl status <...>". If TEST_BUSCTL_STATUS_RC is 94 | # unset or empty, assume this is the call from the "main" function in 95 | # update-systemd-resolved, rather than an automated test checking the 96 | # behaviour of "busctl status <...>". 97 | case "${1-}" in 98 | status) 99 | if [[ -n ${TEST_BUSCTL_STATUS_RC-} ]]; then 100 | busctl_called=1 101 | _log "busctl status: returning ${TEST_BUSCTL_STATUS_RC}" 102 | return "$TEST_BUSCTL_STATUS_RC" 103 | else 104 | return 0 105 | fi 106 | ;; 107 | esac 108 | 109 | shift 4 110 | _log "busctl called with: ${*@Q}" 111 | # Set that busctl has been called 112 | busctl_called=1 113 | 114 | if (("$#" < 1)); then 115 | _fail "busctl called without arguments" 116 | return 117 | fi 118 | 119 | local call="$1" 120 | shift 121 | 122 | local indir 123 | busctl2var indir "$call" || return 124 | 125 | if [[ -v $indir ]]; then 126 | local -a expected=() 127 | local -a expanded 128 | read -r -a expanded <<< "${!indir}" 129 | 130 | if (("$#" > 0)); then 131 | local signature="$1" 132 | 133 | local elem etype 134 | local -i i 135 | for ((i = 0; i < "${#expanded[@]}"; i++)); do 136 | elem="${expanded[${i}]}" 137 | etype="${signature:i:1}" 138 | 139 | # Support "true" and "false" for boolstrings; we expect these to be 140 | # coerced to "yes" and "no", respectively. Similarly, support "yes" 141 | # and "no" for booleans; we expect these to be coerced to "true" and 142 | # "false", respectively. 143 | case "${elem,,}" in 144 | true) 145 | if [[ $etype == s ]]; then 146 | elem=yes 147 | fi 148 | ;; 149 | false) 150 | if [[ $etype == s ]]; then 151 | elem=no 152 | fi 153 | ;; 154 | yes) 155 | if [[ $etype == b ]]; then 156 | elem=true 157 | fi 158 | ;; 159 | no) 160 | if [[ $etype == b ]]; then 161 | elem=false 162 | fi 163 | ;; 164 | default) 165 | elem="" 166 | ;; 167 | esac 168 | 169 | expected+=("$elem") 170 | done 171 | else 172 | expected=("${expanded[@]}") 173 | fi 174 | fi 175 | 176 | case "$call" in 177 | SetLinkDNS) ;; 178 | FlushCaches | ResetServerFeatures | ResetStatistics) 179 | argdesc=zero 180 | argcount=0 181 | ;; 182 | SetLinkDomains | SetLinkDNSSEC | SetLinkDNSOverTLS | SetLinkDefaultRoute | SetLinkLLMNR | SetLinkMulticastDNS) 183 | argdesc=three 184 | argcount=3 185 | ;; 186 | RevertLink) 187 | # Called upon `down` action 188 | return 189 | ;; 190 | *) 191 | _fail "Unknown command called on busctl: ${call}" 192 | ;; 193 | esac 194 | 195 | # XXX *NOT* `[[ -v expected ]]`, which returns a nonzero status when the 196 | # `expected` is declared but empty... 197 | if ! declare -p expected &> /dev/null; then 198 | _fail "${call} was called unexpectedly" 199 | return 200 | elif [[ ${expected[0]:-} == SKIP ]]; then 201 | return 202 | elif (("$#" != "${argcount:-${#}}")); then 203 | _fail "${call} must be called with exactly ${argdesc?} (${argcount?}) arguments" 204 | elif (("${argcount:-0}" > 0)) && [[ ${expected[0]} != "${if_index?}" ]]; then 205 | _fail "${call} not called with the expected interface index as first argument: expected ${if_index}, got ${expected[0]}" 206 | else 207 | local -a actual=("${@:2:$(("$#" - 1))}") 208 | 209 | local actual_size="${#expected[@]}" 210 | local expected_size="${#actual[@]}" 211 | local max 212 | 213 | if ((actual_size > expected_size)); then 214 | max="$actual_size" 215 | else 216 | max="$expected_size" 217 | fi 218 | 219 | local i actual_arg expected_arg 220 | for ((i = 0; i < max; i++)); do 221 | actual_arg="${actual[i]:-''}" 222 | expected_arg="${expected[i]:-''}" 223 | if [[ $actual_arg != "$expected_arg" ]]; then 224 | _fail "${call} not called with the expected setting value in position ${i}: expected ${expected_arg@Q}, got ${actual_arg@Q}" 225 | fi 226 | done 227 | fi 228 | } 229 | 230 | ip() { 231 | _log "ip called with: ${*@Q}" 232 | 233 | if [[ "${1} ${2} ${3} ${4}" == "link show dev ${dev}" ]]; then 234 | _pass "ip was called correctly" 235 | else 236 | _fail "ip was called with incorrect or unknown arguments" 237 | fi 238 | 239 | # Return fake ip statement 240 | echo -e "${ip_ifindex}: ${dev}: " \ 241 | " mtu 1500 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen" \ 242 | " 100\n link/none" 243 | } 244 | 245 | resolvectl() { 246 | _log "resolvectl called with: ${*@Q}" 247 | } 248 | 249 | logger() { 250 | # Remove standard options 251 | if [[ $* == *' --' ]]; then 252 | set -- 253 | else 254 | while (("$#" > 0)); do 255 | case "$1" in 256 | --) 257 | shift 258 | break 259 | ;; 260 | *) 261 | shift 262 | ;; 263 | esac 264 | done 265 | fi 266 | 267 | if (("$#" == 0)) && ! [[ -t 0 ]]; then 268 | local message 269 | while read -r message; do 270 | _log "-- ${message}" 271 | done 272 | else 273 | _log "-- $*" 274 | fi 275 | } 276 | 277 | exit() { 278 | # Override "exit" builtin. Note that "exit" is equivalent to "exit $?", so 279 | # handle that case. 280 | _log "exit called with status ${1:-$?}" 281 | } 282 | 283 | _log() { 284 | (echo >&2 -e " ${DARK}${*}${RESET}") 285 | } 286 | 287 | _pass() { 288 | COUNT_PASS=$((COUNT_PASS + 1)) 289 | (echo >&2 -e " ${GREEN}${PASS}${RESET} ${*}") 290 | } 291 | 292 | _fail() { 293 | COUNT_FAIL=$((COUNT_FAIL + 1)) 294 | if [[ -v TEST ]]; then 295 | FAILED["$TEST"]=1 296 | fi 297 | (echo >&2 -e " ${RED}${FAIL} ${*}${RESET}") 298 | } 299 | 300 | checktest() { 301 | # Increment counter so that we don't double-execute if a test script calls 302 | # this function. 303 | : ${RUNTEST_CALLED:=0} 304 | ((RUNTEST_CALLED += 1)) 305 | 306 | echo -e "${GREEN}- Testing ${TEST_TITLE:-a nameless test}${RESET}" 307 | 308 | if (($# < 1)); then 309 | set -- source update-systemd-resolved 310 | fi 311 | 312 | # Source, don't run, so we don't need to export and internal functions override 313 | # external calls out to system commands 314 | exit_status=0 315 | "$@" || exit_status="$?" 316 | exit_message="script exited with a ${exit_status} exit status" 317 | 318 | if [[ "$((exit_status > 0))" == "${EXPECT_FAILURE:-0}" ]]; then 319 | _pass "$exit_message" 320 | else 321 | _fail "$exit_message" 322 | fi 323 | } 324 | 325 | runtest() { 326 | checktest 327 | 328 | if [[ ${TEST_BUSCTL_CALLED-} == 0 ]]; then 329 | if ((busctl_called == 0)); then 330 | _pass "busctl was not called, as expected" 331 | else 332 | _fail "busctl was called, not expected" 333 | fi 334 | elif ((busctl_called == 0)); then 335 | _fail "busctl was not called, not expected" 336 | fi 337 | 338 | echo 339 | } 340 | 341 | evaltest() { 342 | TEST="${1?}" 343 | 344 | # Set/Reset loop variables 345 | RUNTEST_CALLED=0 346 | EXPECT_FAILURE=0 347 | busctl_called=0 348 | # Set/Reset expected results 349 | # We don't expect any `busctl` calls by default... 350 | unset "${!TEST_BUSCTL_@}" 351 | # Except for `FlushCaches`, which should be called with no arguments. 352 | TEST_BUSCTL_FLUSH_CACHES="" 353 | # Set/Reset expected `busctl` exit status 354 | unset TEST_BUSCTL_STATUS_RC 355 | 356 | # Keep this random, as we will never know the ifindex up-front 357 | ip_ifindex=$((RANDOM %= 64)) 358 | 359 | # Same for the device 360 | dev="tun${RANDOM}" 361 | 362 | # Clear foreign_option_* 363 | unset "${!foreign_option_@}" 364 | 365 | # Import the test configuration 366 | # shellcheck source-path=SCRIPTDIR source=tests/01_no_updates.sh 367 | source "$TEST" || return 368 | 369 | if ((RUNTEST_CALLED > 0)); then 370 | return 371 | fi 372 | 373 | declare -a foreign_options || return 374 | 375 | local i=0 376 | local opt 377 | for opt in "${foreign_options[@]}" in; do 378 | declare "foreign_option_$((i += 1))=${opt}" 379 | done 380 | 381 | runtest 382 | } 383 | 384 | echo "update-systemd-resolved Test Suite" 385 | echo 386 | 387 | if (("$#" < 1)); then 388 | set -- ./tests 389 | fi 390 | 391 | for path in "$@"; do 392 | if [[ -d $path ]]; then 393 | for test in "$path"/*.sh; do 394 | if [[ -f $test ]]; then 395 | evaltest "$test" 396 | fi 397 | done 398 | else 399 | evaltest "$path" 400 | fi 401 | done 402 | 403 | echo -e " ${GREEN}${PASS} ${COUNT_PASS} Passed${RESET}" 404 | echo -e " ${RED}${FAIL} ${COUNT_FAIL} Failed${RESET}" 405 | 406 | if [[ -v FAILED ]] && (("${#FAILED[@]}" > 0)); then 407 | echo -e "\n ${YELLOW}The following files have failing test cases:${RESET}" 408 | 409 | for failed in "${!FAILED[@]}"; do 410 | echo -e " ${ORANGE}${failed}${RESET}" 411 | done 412 | fi 413 | 414 | # Make sure we fail if there are failed tests 415 | ((COUNT_FAIL == 0)) 416 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (2025.03.23) 4 | 5 | ### IMPROVEMENTS 6 | 7 | - Expose a [Nix flake](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html). 8 | This flake's outputs include [the `update-systemd-resolved` Nix package](/nix/packages.nix), as 9 | well as [the `update-systemd-resolved` NixOS module](/nix/nixos-modules.nix) 10 | (module docs are [here](/docs/nixos-modules.md)). 11 | - Support additional DBus calls `ResetServerFeatures`, `ResetStatistics`, 12 | `DNSDefaultRoute`, `SetLinkDNSOverTLS`, `SetLinkLLMNR`, 13 | `SetLinkMulticastDNS`, and `SetLinkNegativeDNSSECTrustAnchors` 14 | ([#110](https://github.com/jonathanio/update-systemd-resolved/pull/110])). 15 | - Check that the `org.freedesktop.resolve1` endpoint is available and 16 | short-circuit with an error message if not 17 | ([#105](https://github.com/jonathanio/update-systemd-resolved/pull/105)). 18 | - Add a `print-polkit-rules` subcommand that generates a polkit rules 19 | specification allowing the specified users and/or groups to perform the DBus 20 | call necessary for `update-systemd-resolved`'s proper operation 21 | ([#100](https://github.com/jonathanio/update-systemd-resolved/pull/100)). 22 | - Support logging without `/dev/log`/`logger` 23 | - ([#115](https://github.com/jonathanio/update-systemd-resolved/pull/115)). 24 | - Avoid doubled log output in the system journal (reported by @VannTen in 25 | [#81](https://github.com/jonathanio/update-systemd-resolved/issues/81), 26 | fixed in [#115](https://github.com/jonathanio/update-systemd-resolved/pull/115)). 27 | - Improve FHS compliance by installing `update-systemd-resolved` to 28 | `/usr/local/bin` by default, rather than to `/usr/local/bin` 29 | (@bowlofeggs, [#106](https://github.com/jonathanio/update-systemd-resolved/pull/106)). 30 | - Add links to Debian and Ubuntu packages (@perlun, 31 | [#112](https://github.com/jonathanio/update-systemd-resolved/pull/112)). 32 | - Flush caches with `busctl` rather than with `resolvectl --flush-caches` 33 | (@cmadamsgit, [#99](https://github.com/jonathanio/update-systemd-resolved/pull/99)). 34 | 35 | ### BUG FIXES 36 | 37 | - `update-systemd-resolved` now accepts IPv6 addresses that do not conform to 38 | [RFC5952](https://tools.ietf.org/html/rfc5952), rather than complaining and 39 | bailing out (reported in 40 | [#76](https://github.com/jonathanio/update-systemd-resolved/pull/76), fixed 41 | in [#104](https://github.com/jonathanio/update-systemd-resolved/pull/104)). 42 | 43 | ### BACKWARDS INCOMPATIBILITIES 44 | 45 | - The use of `setenv PATH ...` in the example `update-systemd-resolved.conf` 46 | and elsewhere is now deprecated. OpenVPN setups that include the example 47 | configuration file (`config /path/to/example/update-systemd-resolved.conf`) 48 | may break if they rely on this now-deprecated `PATH` definition. 49 | - The default installation paths have changed. `update-systemd-resolved` is 50 | now installed to `/usr/local/libexec/openvpn/update-systemd-resolved`, 51 | the example `update-systemd-resolved.conf` is installed to 52 | `/usr/share/doc/openvpn/update-systemd-resolved.conf`. This reflects, among 53 | other things, changes to the Makefile variables that influence installation 54 | paths; for instance, `PREFIX` no longer includes a `/bin` component. The 55 | Makefile now additionally defines and uses the variables `EXEC_PREFIX`, 56 | `LIBEXECDIR`, `DATAROOTDIR`, and `DATADIR`. 57 | - `dhcp-option` invocations are now split on whitespace (the `[[:space:]]` 58 | POSIX character class, to be more specific) rather than being split on single 59 | space characters. 60 | - `dhcp-option` invocations without an argument (that is, `dhcp-option FOO` 61 | rather than, say, `dhcp-option FOO bar`) are now treated as having the empty 62 | string as their value; previously, they were treated as having the option 63 | name as their value (`dhcp-option FOO` == `dhcp-option FOO FOO`). 64 | - `update-systemd-resolved` now requires Bash >= 4.3. 65 | - `update-systemd-resolved` no longer uses the `emerg` log level with the 66 | for logging with the `logger` command, so certain messages are no longer 67 | broadcast to `(p|t)ty`s ([#109](https://github.com/jonathanio/update-systemd-resolved/pull/109])). 68 | 69 | ## 1.3.0 (2019.05.19) 70 | 71 | ### NOTES 72 | 73 | A number of pull-requests and updates added, fixing some bugs and adding new 74 | features. 75 | 76 | ### IMPROVEMENTS 77 | 78 | - Added support for DNS6 option which can take only IPv6 addresses 79 | (@thecodingrobot) 80 | - Based on some feedback by (@tbaumann), alter the handling of script_type and 81 | dev within the body in the main() function to allow it to work more 82 | effectively between the environment and command-line parameters. 83 | - The DNS caches are now flushed when the script as made the configuration 84 | changes for the link (@Edu4rdSHL) 85 | - Change the handling of DOMAIN to support multiple options, with a change in the 86 | way the values are processed and added to systemd-resolved (@adq) 87 | - Updated the documentation in a number of areas, including a new section 88 | specifically on DNS Leakage, links to the DBus commands, NetworkManager and 89 | DNSSEC issues, and spelling corrections, etc. (Thanks to @bohlstry and 90 | @dannyk81 for the help with a script for NetworkManager) 91 | - Now recommended using the `up-restart` option in the configuration files to 92 | ensure that `update-systemd-resolved` is re-run when the connection only 93 | partially restarts (i.e connection restarts, but not the TUN/TAP device). 94 | 95 | ### BACKWARDS INCOMPATIBILITIES 96 | 97 | - The DOMAIN option now supports multiple calls, and rather than the last 98 | provided version being the primary domain for the link, the first value is the 99 | primary domain, and all subsequent calls are added as the equivalent of 100 | DOMAIN-SEARCH. 101 | 102 | ## 1.2.7 (2017.11.12) 103 | 104 | ### NOTES 105 | 106 | Following a request by @JoshDobbin, support has been added for passing 107 | `ADAPTER_DOMAIN_SUFFIX` via `dhcp-options` to work with the Microsoft standard. 108 | Also included some additional notes in README.md about using `down` in dropped 109 | privilege situations for clarification. 110 | 111 | ### IMPROVEMENTS 112 | 113 | - Added support for ADAPTER_DOMAIN_SUFFIX (@jonathanio) 114 | - Added notes in README.md about `down` with dropped privileges (@jonathanio) 115 | 116 | ## 1.2.6 (2017.07.24) 117 | 118 | ### NOTES 119 | 120 | Improvements made to the `logger` command to prevent issues with privilege 121 | dropping under the assistance of @dermarens, @terminalmage, @guruxu, and @benvh. 122 | Updated some documentation for consistency and clarity. Thanks to @flungo and 123 | @dawansv here. 124 | 125 | ### IMPROVEMENTS 126 | 127 | - Updated to include a full list in PATH, including sbin paths. (@jonathanio) 128 | - Updated documentation regarding DNS leakage. (@jonathanio) 129 | - Updated all script locations to be consistent. (@jonathanio) 130 | - Add some installation instructions to README.md. (@flungo) 131 | - Update command-line parameters needed within Makefile/README.md. (@noraj1337) 132 | - Fix script name in command-line path within README.md. (@phR0ze) 133 | 134 | ## 1.2.5 (2017.03.02) 135 | 136 | ### IMPROVEMENTS 137 | 138 | - Updated to include a full list in PATH, including sbin paths. (@jonathanio) 139 | 140 | ## 1.2.4 (2017.03.02) 141 | 142 | ### NOTES 143 | 144 | @piotr-dobrogost, @mgu, and @aRkadeFR helped improve the documentation. 145 | 146 | ### IMPROVEMENTS 147 | 148 | - It was noted that the PATH setting used in the documentation doesn't work on 149 | all systems (sorry, my bad), so it has now been updated so it should now work. 150 | (@aRkadeFR) 151 | 152 | ## 1.2.3 (2016.12.25) 153 | 154 | ### NOTES 155 | 156 | @Nauxuron provided a patch to improve DESTDIR and PREFIX handling in Makefile. 157 | 158 | ### IMPROVEMENTS 159 | 160 | - Improve handling of DESTDIR and PREFIX in the Makefile to follow the GNU 161 | guidelines. (@Nauxuron) 162 | 163 | ## 1.2.2 (2016.12.13) 164 | 165 | ### NOTES 166 | 167 | This one is a thanks to @mikken and helps support OpenVPN 2.4 as well as fix 168 | an issue with `DNSSEC` handling on the `busctl` call. 169 | 170 | ### BUG FIXES 171 | 172 | - The incorrect usage of `down-pre` which as of OpenVPN 2.4 is now a fatal error 173 | when you pass it an argument (i.e. the script we were originally thought it 174 | should be calling). (@mikken) 175 | - Issues with `busctl` and bash properly handling the "empty string" case to use 176 | the default `DNSSEC` option. (@jonathanio) 177 | - Noise when `busctl` is called on the down case when privileges have been 178 | dropped in the client. (@mikken) 179 | - Added documentation for `allow-downgrade` support in `DNSSEC` option (which 180 | was supported, but not documented). (@jonathanio) 181 | 182 | ## 1.2.1 (2016.10.06) 183 | 184 | ### NOTES 185 | 186 | Thanks for @arjenschol for spotting this one: An error in the AF_INET value 187 | provided to SetLinkDNS prevented IPv6 DNS servers from being added. 188 | 189 | ### BUG FIXES 190 | 191 | - Fix IPv6 DNS by specifying AF_INET6 value (10) insteadof array size (2) 192 | (@arjenschol) 193 | 194 | ## 1.2.0 (2016.08.29) 195 | 196 | ### NOTES 197 | 198 | Add support for DNSSEC processing, improve logic around `DOMAIN` and 199 | `DOMAIN-SEARCH` handling, add support for `DOMAIN-ROUTE`, and improve 200 | documentation. 201 | 202 | ### BACKWARDS INCOMPATIBILITIES 203 | 204 | - Due to (probably) an incorrect assumption on my part (@jonathanio) in the 205 | purpose of `DOMAIN-SEARCH` verses `DOMAIN`, domains added via `DOMAIN` were 206 | marked as searchable, and so would be appended to bare domain names, while 207 | those added via `DOMAIN-SEARCH` would not. This was a divergance from how 208 | older OpenVPN handler scripts (such as `update-resolv-conf` and 209 | `update-systemd-network`) processed them (i.e. in all cases they were just 210 | made searchable). Note that both scripts didn't really have the concept of 211 | `domain` in the same way as `/etc/resolv.conf` understood it. This script now 212 | (hopefully) properly handles `DOMAIN` and `DOMAIN-SEARCH` (single of the 213 | former, and is primary, multiple of the latter and secondary). 214 | 215 | ### FEATURES 216 | 217 | - Add support for `DNSSEC` option which allows you to enable or disable (or 218 | leave to system default) the `DNSSEC` setting for any DNS queries made to the 219 | DNS servers provided for this link. (@jonathanio) 220 | - Add support for `DOMAIN-ROUTE` which, through `systemd-resolved`, allows you 221 | to set domain names which should be routed over this link to the DNS servers 222 | provided. (@jonathanio) 223 | 224 | ### IMPROVEMENTS 225 | 226 | - Correct the logic around the handling of `DOMAIN` and `DOMAIN-SEARCH` to be 227 | more compatible with previous versions of these handlers. (@jonathanio) 228 | 229 | ## 1.1.1 (2016.08.10) 230 | 231 | ### NOTES 232 | 233 | Thanks to the help from @pid1 for this release. The documentation mistakenly 234 | noted to use pre-down for the script now (compared to down originally, which 235 | failed as the tun or tap device would have been removed before the script 236 | ran). However, this should have in fact been down-pre. 237 | 238 | ### BUG FIXES 239 | 240 | - Fix `pre-down` to `down-pre` in the documentation else you'll break your 241 | OpenVPN configuration. (@pid1) 242 | 243 | ## 1.1.0 (2016.08.08) 244 | 245 | ### NOTES 246 | 247 | Thanks to the work by @BaxterStockman, the script has been refactored, hopefully 248 | making it easier to read and follow, while additional tests around IPv6 249 | processing have been added. 250 | 251 | ### IMPROVEMENTS 252 | 253 | - Refactor the codebase to make it easier to read and expand. (@BaxterStockman) 254 | - Improve run-tests so multiple tests can be run within a file, and can expect 255 | failures within a test. (@BaxterStockman) 256 | - Add tests for invalid IPv6 addresses. (@BaxterStockman) 257 | 258 | ## 1.0.0 (2016.06.23) 259 | 260 | ### NOTES 261 | 262 | First release of `update-systemd-resolved`. Should fully support the three 263 | standard DHCP options in OpenVPN (`DNS`, `DOMAIN`, and `DOMAIN-SEARCH`) with 264 | integration tests around the code to manage and monitor regressions. Also 265 | supports multiple (and combined) IPv4 and IPv6 DNS addresses. 266 | -------------------------------------------------------------------------------- /docs/nixos-modules.md: -------------------------------------------------------------------------------- 1 | ## programs\.update-systemd-resolved\.package 2 | 3 | The update-systemd-resolved package to use\. 4 | 5 | 6 | 7 | *Type:* 8 | package 9 | 10 | 11 | 12 | *Default:* 13 | ` pkgs.update-systemd-resolved ` 14 | 15 | *Declared by:* 16 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 17 | 18 | 19 | 20 | ## programs\.update-systemd-resolved\.servers 21 | 22 | 23 | 24 | Attribute set of ` update-systemd-resolved ` configurations\. 25 | Intended to be included in 26 | ` services.openvpn.servers..config ` entries\. 27 | 28 | 29 | 30 | *Type:* 31 | attribute set of (submodule) 32 | 33 | 34 | 35 | *Default:* 36 | ` { } ` 37 | 38 | *Declared by:* 39 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 40 | 41 | 42 | 43 | ## programs\.update-systemd-resolved\.servers\.\\.config 44 | 45 | 46 | 47 | The configuration text for inclusion in 48 | ` services.openvpn.servers..config `\. 49 | 50 | 51 | 52 | *Type:* 53 | strings concatenated with “\\n” *(read only)* 54 | 55 | *Declared by:* 56 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 57 | 58 | 59 | 60 | ## programs\.update-systemd-resolved\.servers\.\\.configFile 61 | 62 | 63 | 64 | A configuration file containing 65 | ` programs.update-systemd-resolved.servers..config ` 66 | for inclusion in ` services.openvpn.servers..config ` 67 | via the ` config ` directive\. 68 | 69 | 70 | 71 | *Type:* 72 | absolute path *(read only)* 73 | 74 | 75 | 76 | *Default:* 77 | ` "/nix/store/-update-systemd-resolved-.conf" ` 78 | 79 | *Declared by:* 80 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 81 | 82 | 83 | 84 | ## programs\.update-systemd-resolved\.servers\.\\.includeAutomatically 85 | 86 | 87 | 88 | Whether to include the generated configuration in 89 | ` services.openvpn.servers..config `\. 90 | 91 | 92 | 93 | *Type:* 94 | boolean 95 | 96 | 97 | 98 | *Default:* 99 | ` false ` 100 | 101 | *Declared by:* 102 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 103 | 104 | 105 | 106 | ## programs\.update-systemd-resolved\.servers\.\\.openvpnServerName 107 | 108 | 109 | 110 | ` ` in 111 | ` services.openvpn.servers..config `\. 112 | 113 | 114 | 115 | *Type:* 116 | string 117 | 118 | 119 | 120 | *Default:* 121 | ` "‹name›" ` 122 | 123 | *Declared by:* 124 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 125 | 126 | 127 | 128 | ## programs\.update-systemd-resolved\.servers\.\\.pushSettings 129 | 130 | 131 | 132 | Whether to push ` update-system-resolved ` 133 | settings with OpenVPN’s ` push ` directive\. 134 | Enable this if the target OpenVPN instance is a server; 135 | disable it if the target instance is a client\. 136 | 137 | 138 | 139 | *Type:* 140 | boolean 141 | 142 | 143 | 144 | *Default:* 145 | ` false ` 146 | 147 | *Declared by:* 148 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 149 | 150 | 151 | 152 | ## programs\.update-systemd-resolved\.servers\.\\.settings 153 | 154 | 155 | 156 | DNS-related settings for this VPN’s link\. 157 | 158 | 159 | 160 | *Type:* 161 | submodule 162 | 163 | 164 | 165 | *Default:* 166 | ` { } ` 167 | 168 | *Declared by:* 169 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 170 | 171 | 172 | 173 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.defaultRoute 174 | 175 | 176 | 177 | Whether to use the DNS servers configured for 178 | this link to resolve queries for domains not 179 | explicitly assigned to the servers on any other 180 | link\. 181 | 182 | See [` resolvectl(1) `](https://www.freedesktop.org/software/systemd/man/resolvectl.html)'s coverage of ` default-route ` for a 183 | description of this feature\. 184 | 185 | 186 | 187 | *Type:* 188 | (one of \, “yes”, “no”) or boolean convertible to it 189 | 190 | 191 | 192 | *Default:* 193 | ` true ` 194 | 195 | *Declared by:* 196 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 197 | 198 | 199 | 200 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns 201 | 202 | 203 | 204 | Attribute set naming DNS servers to configure for 205 | this VPN’s link\. 206 | 207 | See the description of ` DNS ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 208 | the meaning of this option and its available values\. 209 | 210 | 211 | 212 | *Type:* 213 | attribute set of ((submodule) or non-empty string convertible to it) 214 | 215 | 216 | 217 | *Default:* 218 | ` { } ` 219 | 220 | 221 | 222 | *Example:* 223 | 224 | ``` 225 | { 226 | "3.4.5.6" = { }; 227 | resolver-the-first = { 228 | address = "1.2.3.4"; 229 | port = 5353; 230 | }; 231 | resolver-the-second = "2.3.4.5"; 232 | } 233 | ``` 234 | 235 | *Declared by:* 236 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 237 | 238 | 239 | 240 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns\.\\.__toString 241 | 242 | 243 | 244 | String representation of the DNS server\. 245 | 246 | 247 | 248 | *Type:* 249 | function that evaluates to a(n) string *(read only)* 250 | 251 | 252 | 253 | *Default:* 254 | ` ` 255 | 256 | *Declared by:* 257 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 258 | 259 | 260 | 261 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns\.\\.address 262 | 263 | 264 | 265 | The IPv4 or IPv6 address of the DNS server\. 266 | 267 | 268 | 269 | *Type:* 270 | non-empty string 271 | 272 | 273 | 274 | *Default:* 275 | ` "‹name›" ` 276 | 277 | *Declared by:* 278 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 279 | 280 | 281 | 282 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns\.\\.interface 283 | 284 | 285 | 286 | Network interface name or index (note that this is as 287 | detailed as [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) gets about the 288 | meaning of the interface component of a DNS server 289 | specification)\. 290 | 291 | 292 | 293 | *Type:* 294 | null or non-empty string 295 | 296 | 297 | 298 | *Default:* 299 | ` null ` 300 | 301 | *Declared by:* 302 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 303 | 304 | 305 | 306 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns\.\\.port 307 | 308 | 309 | 310 | The port number of the DNS server\. 311 | 312 | 313 | 314 | *Type:* 315 | null or 16 bit unsigned integer; between 0 and 65535 (both inclusive) 316 | 317 | 318 | 319 | *Default:* 320 | ` null ` 321 | 322 | *Declared by:* 323 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 324 | 325 | 326 | 327 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dns\.\\.sni 328 | 329 | 330 | 331 | Server name indication to send when using DNS-over-TLS\. 332 | 333 | 334 | 335 | *Type:* 336 | null or non-empty string 337 | 338 | 339 | 340 | *Default:* 341 | ` null ` 342 | 343 | *Declared by:* 344 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 345 | 346 | 347 | 348 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dnsOverTLS 349 | 350 | 351 | 352 | Whether to enable DNS-over-TLS for this link\. 353 | 354 | See the description of ` DNSOverTLS ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 355 | the meaning of this option and its available values\. 356 | 357 | In addition to the values documented there, this option also 358 | accepts the value “default”, signifying that this link should use 359 | the global value for ` DNSOverTLS ` configured in ` resolved.conf `\. 360 | 361 | 362 | 363 | *Type:* 364 | (one of \, “default”, “opportunistic”, “yes”, “no”) or boolean convertible to it 365 | 366 | 367 | 368 | *Default:* 369 | ` null ` 370 | 371 | *Declared by:* 372 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 373 | 374 | 375 | 376 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dnssec 377 | 378 | 379 | 380 | Whether to enable DNSSEC for this link\. 381 | 382 | See the description of ` DNSSEC ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 383 | the meaning of this option and its available values\. 384 | 385 | In addition to the values documented there, this option also 386 | accepts the value “default”, signifying that this link should use 387 | the global value for ` DNSSEC ` configured in ` resolved.conf `\. 388 | 389 | 390 | 391 | *Type:* 392 | (one of \, “allow-downgrade”, “default”, “yes”, “no”) or boolean convertible to it 393 | 394 | 395 | 396 | *Default:* 397 | ` null ` 398 | 399 | *Declared by:* 400 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 401 | 402 | 403 | 404 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.dnssecNegativeTrustAnchors 405 | 406 | 407 | 408 | DNSSEC negative trust anchors to configure for 409 | this link\. See the ` NEGATIVE TRUST ANCHORS ` 410 | section in ` dnssec-trust-anchors.d ` for 411 | a description of negative trust anchors and how 412 | to specify them\. 413 | 414 | 415 | 416 | *Type:* 417 | list of non-empty string 418 | 419 | 420 | 421 | *Default:* 422 | ` [ ] ` 423 | 424 | *Declared by:* 425 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 426 | 427 | 428 | 429 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.domain 430 | 431 | 432 | 433 | Main domain to configure for this link\. 434 | 435 | See the description of ` Domains ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 436 | the meaning of this option and its available values\. 437 | 438 | 439 | 440 | *Type:* 441 | null or non-empty string 442 | 443 | 444 | 445 | *Default:* 446 | ` null ` 447 | 448 | *Declared by:* 449 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 450 | 451 | 452 | 453 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.flushCaches 454 | 455 | 456 | 457 | Whether to flush ` systemd-resolved `’s cache upon 458 | starting the VPN\. 459 | 460 | See [` resolvectl(1) `](https://www.freedesktop.org/software/systemd/man/resolvectl.html)'s coverage of ` flush-caches ` for a 461 | description of this feature\. 462 | 463 | 464 | 465 | *Type:* 466 | (one of \, “yes”, “no”) or boolean convertible to it 467 | 468 | 469 | 470 | *Default:* 471 | ` null ` 472 | 473 | *Declared by:* 474 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 475 | 476 | 477 | 478 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.llmnr 479 | 480 | 481 | 482 | Whether to enable LLMNR for this link\. 483 | 484 | See the description of ` LLMNR ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 485 | the meaning of this option and its available values\. 486 | 487 | In addition to the values documented there, this option also 488 | accepts the value “default”, signifying that this link should use 489 | the global value for ` LLMNR ` configured in ` resolved.conf `\. 490 | 491 | 492 | 493 | *Type:* 494 | (one of \, “default”, “resolve”, “yes”, “no”) or boolean convertible to it 495 | 496 | 497 | 498 | *Default:* 499 | ` null ` 500 | 501 | *Declared by:* 502 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 503 | 504 | 505 | 506 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.multicastDNS 507 | 508 | 509 | 510 | Whether to enable multicast DNS for this link\. 511 | 512 | See the description of ` MulticastDNS ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 513 | the meaning of this option and its available values\. 514 | 515 | In addition to the values documented there, this option also 516 | accepts the value “default”, signifying that this link should use 517 | the global value for ` MulticastDNS ` configured in ` resolved.conf `\. 518 | 519 | 520 | 521 | *Type:* 522 | (one of \, “default”, “resolve”, “yes”, “no”) or boolean convertible to it 523 | 524 | 525 | 526 | *Default:* 527 | ` null ` 528 | 529 | *Declared by:* 530 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 531 | 532 | 533 | 534 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.resetServerFeatures 535 | 536 | 537 | 538 | Whether to reset learned server features when 539 | bringing up the VPN link\. 540 | 541 | See [` resolvectl(1) `](https://www.freedesktop.org/software/systemd/man/resolvectl.html)'s coverage of ` reset-server-features ` for a 542 | description of this feature\. 543 | 544 | 545 | 546 | *Type:* 547 | (one of \, “yes”, “no”) or boolean convertible to it 548 | 549 | 550 | 551 | *Default:* 552 | ` null ` 553 | 554 | *Declared by:* 555 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 556 | 557 | 558 | 559 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.resetStatistics 560 | 561 | 562 | 563 | Whether to reset the statistics counters shown in 564 | ` resolvectl statistics ` to zero when 565 | bringing up the VPN link\. 566 | 567 | See [` resolvectl(1) `](https://www.freedesktop.org/software/systemd/man/resolvectl.html)'s coverage of ` reset-statistics ` for a 568 | description of this feature\. 569 | 570 | 571 | 572 | *Type:* 573 | (one of \, “yes”, “no”) or boolean convertible to it 574 | 575 | 576 | 577 | *Default:* 578 | ` null ` 579 | 580 | *Declared by:* 581 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 582 | 583 | 584 | 585 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.routeOnlyDomains 586 | 587 | 588 | 589 | List of route-only domains to configure for this 590 | link\. 591 | 592 | See the description of ` Domains ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 593 | the meaning of this option and its available values\. 594 | 595 | 596 | 597 | *Type:* 598 | list of non-empty string 599 | 600 | 601 | 602 | *Default:* 603 | ` [ ] ` 604 | 605 | *Declared by:* 606 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 607 | 608 | 609 | 610 | ## programs\.update-systemd-resolved\.servers\.\\.settings\.searchDomains 611 | 612 | 613 | 614 | List of search domains to configure for this 615 | link\. 616 | 617 | See the description of ` Domains ` in [` resolved.conf(5) `](https://www.freedesktop.org/software/systemd/man/resolved.conf.html) for 618 | the meaning of this option and its available values\. 619 | 620 | 621 | 622 | *Type:* 623 | list of non-empty string 624 | 625 | 626 | 627 | *Default:* 628 | ` [ ] ` 629 | 630 | *Declared by:* 631 | - [nix/nixos-modules\.nix](/nix/nixos-modules.nix) 632 | 633 | 634 | -------------------------------------------------------------------------------- /nix/nixos-modules.nix: -------------------------------------------------------------------------------- 1 | { 2 | self, 3 | moduleWithSystem, 4 | ... 5 | }: { 6 | flake = {config, ...}: { 7 | nixosModules.default = config.nixosModules.update-systemd-resolved; 8 | 9 | nixosModules.update-systemd-resolved = 10 | moduleWithSystem 11 | ( 12 | perSystem @ {config}: nixos @ { 13 | config, 14 | lib, 15 | pkgs, 16 | ... 17 | }: let 18 | inherit (lib) mkOption mkPackageOption types; 19 | 20 | cfg = config.programs.update-systemd-resolved; 21 | 22 | # Convert to a format systemd understands 23 | bool2str = bool: 24 | if bool 25 | then "yes" 26 | else "no"; 27 | 28 | enumOrBool = enum: types.coercedTo types.bool bool2str (types.enum ((lib.toList enum) ++ ["yes" "no"])); 29 | 30 | mkResolvedConfDesc = name: '' 31 | See the description of `${name}` in {manpage}`resolved.conf(5)` for 32 | the meaning of this option and its available values. 33 | ''; 34 | 35 | mkResolvedConfDescWithDefault = name: '' 36 | ${mkResolvedConfDesc name} 37 | 38 | In addition to the values documented there, this option also 39 | accepts the value "default", signifying that this link should use 40 | the global value for `${name}` configured in `resolved.conf`. 41 | ''; 42 | 43 | mkResolvectlDesc = cmd: '' 44 | See {manpage}`resolvectl(1)`'s coverage of {command}`${cmd}` for a 45 | description of this feature. 46 | ''; 47 | 48 | dnsModule = types.submodule ({name, ...}: { 49 | options = { 50 | address = mkOption { 51 | type = types.nonEmptyStr; 52 | default = name; 53 | description = '' 54 | The IPv4 or IPv6 address of the DNS server. 55 | ''; 56 | }; 57 | 58 | port = mkOption { 59 | type = types.nullOr types.port; 60 | default = null; 61 | description = '' 62 | The port number of the DNS server. 63 | ''; 64 | }; 65 | 66 | interface = mkOption { 67 | type = types.nullOr types.nonEmptyStr; 68 | default = null; 69 | description = '' 70 | Network interface name or index (note that this is as 71 | detailed as {manpage}`resolved.conf(5)` gets about the 72 | meaning of the interface component of a DNS server 73 | specification). 74 | ''; 75 | }; 76 | 77 | sni = mkOption { 78 | type = types.nullOr types.nonEmptyStr; 79 | default = null; 80 | description = '' 81 | Server name indication to send when using DNS-over-TLS. 82 | ''; 83 | }; 84 | 85 | __toString = mkOption { 86 | type = types.functionTo types.str; 87 | readOnly = true; 88 | description = '' 89 | String representation of the DNS server. 90 | ''; 91 | default = self: let 92 | looksLikeIPv6 = lib.hasInfix ":" self.address; 93 | hasPort = self.port != null; 94 | address = 95 | if looksLikeIPv6 && hasPort 96 | then "[${self.address}]" 97 | else self.address; 98 | in 99 | lib.concatStrings ([ 100 | address 101 | ] 102 | ++ lib.optional hasPort ":${toString self.port}" 103 | ++ lib.optional (self.interface != null) "%${self.interface}" 104 | ++ lib.optional (self.sni != null) "#${self.sni}"); 105 | }; 106 | }; 107 | }); 108 | 109 | dnsType = types.coercedTo types.nonEmptyStr (address: {inherit address;}) dnsModule; 110 | in { 111 | options = { 112 | programs.update-systemd-resolved = { 113 | package = mkPackageOption perSystem.config.packages "update-systemd-resolved" {}; 114 | 115 | servers = mkOption { 116 | default = {}; 117 | description = '' 118 | Attribute set of `update-systemd-resolved` configurations. 119 | Intended to be included in 120 | {option}`services.openvpn.servers..config` entries. 121 | ''; 122 | type = types.attrsOf (types.submodule ({ 123 | name, 124 | config, 125 | ... 126 | }: { 127 | options = { 128 | openvpnServerName = mkOption { 129 | type = types.str; 130 | default = name; 131 | description = '' 132 | `` in 133 | {option}`services.openvpn.servers..config`. 134 | ''; 135 | }; 136 | 137 | includeAutomatically = mkOption { 138 | type = types.bool; 139 | default = false; 140 | description = '' 141 | Whether to include the generated configuration in 142 | {option}`services.openvpn.servers..config`. 143 | ''; 144 | }; 145 | 146 | pushSettings = mkOption { 147 | type = types.bool; 148 | default = false; 149 | description = '' 150 | Whether to push {command}`update-system-resolved` 151 | settings with OpenVPN's {command}`push` directive. 152 | Enable this if the target OpenVPN instance is a server; 153 | disable it if the target instance is a client. 154 | ''; 155 | }; 156 | 157 | config = mkOption { 158 | type = types.lines; 159 | readOnly = true; 160 | description = '' 161 | The configuration text for inclusion in 162 | {option}`services.openvpn.servers..config`. 163 | ''; 164 | }; 165 | 166 | configFile = mkOption { 167 | type = types.path; 168 | readOnly = true; 169 | default = pkgs.writeText "update-systemd-resolved-${name}.conf" config.config; 170 | defaultText = "${toString builtins.storeDir}/-update-systemd-resolved-.conf"; 171 | description = '' 172 | A configuration file containing 173 | {option}`programs.update-systemd-resolved.servers..config` 174 | for inclusion in {option}`services.openvpn.servers..config` 175 | via the {command}`config` directive. 176 | ''; 177 | }; 178 | 179 | settings = mkOption { 180 | default = {}; 181 | 182 | description = '' 183 | DNS-related settings for this VPN's link. 184 | ''; 185 | 186 | type = types.submodule ({...}: { 187 | options = { 188 | # TODO DNS6 189 | dns = mkOption { 190 | type = types.attrsOf dnsType; 191 | default = {}; 192 | example = { 193 | resolver-the-first = { 194 | address = "1.2.3.4"; 195 | port = 5353; 196 | }; 197 | 198 | resolver-the-second = "2.3.4.5"; 199 | 200 | "3.4.5.6" = {}; 201 | }; 202 | description = '' 203 | Attribute set naming DNS servers to configure for 204 | this VPN's link. 205 | 206 | ${mkResolvedConfDesc "DNS"} 207 | ''; 208 | }; 209 | 210 | domain = mkOption { 211 | type = types.nullOr types.nonEmptyStr; 212 | default = null; 213 | description = '' 214 | Main domain to configure for this link. 215 | 216 | ${mkResolvedConfDesc "Domains"} 217 | ''; 218 | }; 219 | 220 | searchDomains = mkOption { 221 | type = types.listOf types.nonEmptyStr; 222 | default = []; 223 | description = '' 224 | List of search domains to configure for this 225 | link. 226 | 227 | ${mkResolvedConfDesc "Domains"} 228 | ''; 229 | }; 230 | 231 | routeOnlyDomains = mkOption { 232 | type = types.listOf types.nonEmptyStr; 233 | default = []; 234 | description = '' 235 | List of route-only domains to configure for this 236 | link. 237 | 238 | ${mkResolvedConfDesc "Domains"} 239 | ''; 240 | }; 241 | 242 | defaultRoute = mkOption { 243 | type = enumOrBool null; 244 | default = true; 245 | description = '' 246 | Whether to use the DNS servers configured for 247 | this link to resolve queries for domains not 248 | explicitly assigned to the servers on any other 249 | link. 250 | 251 | ${mkResolvectlDesc "default-route"} 252 | ''; 253 | }; 254 | 255 | dnsOverTLS = mkOption { 256 | type = enumOrBool [null "default" "opportunistic"]; 257 | default = null; 258 | description = '' 259 | Whether to enable DNS-over-TLS for this link. 260 | 261 | ${mkResolvedConfDescWithDefault "DNSOverTLS"} 262 | ''; 263 | }; 264 | 265 | dnssec = mkOption { 266 | type = enumOrBool [null "allow-downgrade" "default"]; 267 | default = null; 268 | description = '' 269 | Whether to enable DNSSEC for this link. 270 | 271 | ${mkResolvedConfDescWithDefault "DNSSEC"} 272 | ''; 273 | }; 274 | 275 | dnssecNegativeTrustAnchors = mkOption { 276 | type = types.listOf types.nonEmptyStr; 277 | default = []; 278 | description = '' 279 | DNSSEC negative trust anchors to configure for 280 | this link. See the `NEGATIVE TRUST ANCHORS` 281 | section in {manpage}`dnssec-trust-anchors.d` for 282 | a description of negative trust anchors and how 283 | to specify them. 284 | ''; 285 | }; 286 | 287 | flushCaches = mkOption { 288 | type = enumOrBool null; 289 | default = null; 290 | description = '' 291 | Whether to flush `systemd-resolved`'s cache upon 292 | starting the VPN. 293 | 294 | ${mkResolvectlDesc "flush-caches"} 295 | ''; 296 | }; 297 | 298 | llmnr = mkOption { 299 | type = enumOrBool [null "default" "resolve"]; 300 | default = null; 301 | description = '' 302 | Whether to enable LLMNR for this link. 303 | 304 | ${mkResolvedConfDescWithDefault "LLMNR"} 305 | ''; 306 | }; 307 | 308 | multicastDNS = mkOption { 309 | type = enumOrBool [null "default" "resolve"]; 310 | default = null; 311 | description = '' 312 | Whether to enable multicast DNS for this link. 313 | 314 | ${mkResolvedConfDescWithDefault "MulticastDNS"} 315 | ''; 316 | }; 317 | 318 | resetServerFeatures = mkOption { 319 | type = enumOrBool null; 320 | default = null; 321 | description = '' 322 | Whether to reset learned server features when 323 | bringing up the VPN link. 324 | 325 | ${mkResolvectlDesc "reset-server-features"} 326 | ''; 327 | }; 328 | 329 | resetStatistics = mkOption { 330 | type = enumOrBool null; 331 | default = null; 332 | description = '' 333 | Whether to reset the statistics counters shown in 334 | {command}`resolvectl statistics` to zero when 335 | bringing up the VPN link. 336 | 337 | ${mkResolvectlDesc "reset-statistics"} 338 | ''; 339 | }; 340 | }; 341 | }); 342 | }; 343 | }; 344 | 345 | config = { 346 | config = let 347 | renderDHCPOption = let 348 | client = option: value: let 349 | ucOption = lib.toUpper option; 350 | in "dhcp-option ${ucOption} ${toString value}"; 351 | 352 | server = option: value: "push \"${client option value}\""; 353 | in 354 | if config.pushSettings 355 | then server 356 | else client; 357 | 358 | maybeRenderDHCPOption = option: value: 359 | lib.optionalString (value != null) (renderDHCPOption option value); 360 | 361 | renderDHCPOptionList = option: 362 | lib.concatMapStringsSep "\n" (renderDHCPOption option); 363 | in '' 364 | config ${cfg.package}/share/doc/openvpn/update-systemd-resolved.conf 365 | 366 | ${renderDHCPOptionList "dns" (builtins.attrValues config.settings.dns)} 367 | 368 | ${maybeRenderDHCPOption "domain" config.settings.domain} 369 | ${renderDHCPOptionList "domain-route" (config.settings.routeOnlyDomains)} 370 | ${renderDHCPOptionList "domain-search" (config.settings.searchDomains)} 371 | 372 | ${maybeRenderDHCPOption "dnssec" config.settings.dnssec} 373 | ${renderDHCPOptionList "dnssec-negative-trust-anchors" config.settings.dnssecNegativeTrustAnchors} 374 | 375 | ${maybeRenderDHCPOption "default-route" config.settings.defaultRoute} 376 | ${maybeRenderDHCPOption "dns-over-tls" config.settings.dnsOverTLS} 377 | ${maybeRenderDHCPOption "flush-caches" config.settings.flushCaches} 378 | ${maybeRenderDHCPOption "llmnr" config.settings.llmnr} 379 | ${maybeRenderDHCPOption "multicast-dns" config.settings.multicastDNS} 380 | ${maybeRenderDHCPOption "reset-server-features" config.settings.resetServerFeatures} 381 | ${maybeRenderDHCPOption "reset-statistics" config.settings.resetStatistics} 382 | ''; 383 | }; 384 | })); 385 | }; 386 | }; 387 | }; 388 | 389 | config = { 390 | services.openvpn.servers = lib.mapAttrs' (name: value: { 391 | name = value.openvpnServerName; 392 | value = { 393 | config = lib.mkAfter '' 394 | config ${value.configFile} 395 | ''; 396 | }; 397 | }) (lib.filterAttrs (_: s: s.includeAutomatically) cfg.servers); 398 | }; 399 | } 400 | ); 401 | }; 402 | } 403 | -------------------------------------------------------------------------------- /nix/checks.nix: -------------------------------------------------------------------------------- 1 | {self, ...}: { 2 | perSystem = perSystem @ { 3 | config, 4 | lib, 5 | pkgs, 6 | ... 7 | }: { 8 | checks.default = config.checks.update-systemd-resolved; 9 | 10 | checks.docs = 11 | pkgs.runCommand "update-systemd-resolved-docs-check" { 12 | src = self; 13 | } '' 14 | current="''${src}/docs/nixos-modules.md" 15 | target=${config.packages.docs} 16 | 17 | if ! [ -f "$current" ]; then 18 | printf 1>&2 -- 'missing "%s"; please generate documentation with `mkoptdocs`.\n' "$current" 19 | exit 1 20 | elif ! ${pkgs.diffutils}/bin/cmp "$current" "$target"; then 21 | printf 1>&2 -- '"%s" and "%s" differ; please generate documentation with `mkoptdocs`.\n' "$current" "$target" 22 | exit 1 23 | else 24 | touch "$out" 25 | fi 26 | ''; 27 | 28 | checks.update-systemd-resolved = let 29 | # Name of the test script 30 | name = "update-systemd-resolved"; 31 | 32 | # "" in "services.openvpn.servers." 33 | instanceName = "${name}-test"; 34 | 35 | # "" in "systemd.services." 36 | serviceAttrName = "openvpn-${instanceName}"; 37 | 38 | # systemd service name 39 | serviceName = "${serviceAttrName}.service"; 40 | 41 | # "dev " in OpenVPN configs 42 | devName = "tun"; 43 | 44 | # OpenVPN interface name 45 | interface = "${devName}0"; 46 | 47 | # "port " in OpenVPN server config 48 | serverPort = 11194; 49 | 50 | # Addresses assigned to server and client tun interfaces 51 | serverEndpoint = "10.8.0.1"; 52 | clientEndpoint = "10.8.0.2"; 53 | 54 | # "" in "dhcp-option DOMAIN ". Also used for various 55 | # dnsmasq settings. 56 | vpnDomain = "update.systemd.resolved"; 57 | 58 | # RouteDNS listening port 59 | resolverPort = 5353; 60 | 61 | # Try to infer the IP address assigned to a node in this NixOS test 62 | # scenario, falling back to the value of "default" if inference failed. 63 | fetchFirstAddress = { 64 | node, 65 | default, 66 | ifname ? "eth1", 67 | type ? "ipv4", 68 | }: let 69 | addresses = node.networking.interfaces.${ifname}.${type}.addresses or []; 70 | address = 71 | if (builtins.length addresses) > 0 72 | then builtins.head addresses 73 | else {}; 74 | in 75 | address.address or default; 76 | 77 | # Generate polkit rules for allowing unprivileged users to perform 78 | # org.freedesktop.resolve1 actions. 79 | mkPolkitRules = { 80 | user ? "openvpn", 81 | group ? "network", 82 | }: 83 | perSystem.config.packages.update-systemd-resolved.mkPolkitRules { 84 | inherit user group; 85 | }; 86 | 87 | # Wrapper for mkPolkitRules that extracts appropriate arguments from a 88 | # "systemd.services." definition 89 | mkPolkitRulesForService = service: let 90 | sc = service.serviceConfig; 91 | in 92 | mkPolkitRules 93 | (lib.optionalAttrs (sc ? "User") {user = sc.User;}) 94 | // (lib.optionalAttrs (sc ? "Group") {group = sc.Group;}) 95 | // {inherit pkgs lib;}; 96 | in 97 | pkgs.nixosTest { 98 | inherit name; 99 | 100 | nodes = { 101 | resolver = { 102 | networking.domain = vpnDomain; 103 | 104 | networking.firewall = { 105 | allowedTCPPorts = [resolverPort]; 106 | allowedUDPPorts = [resolverPort]; 107 | }; 108 | 109 | services.dnsmasq = { 110 | enable = true; 111 | resolveLocalQueries = false; 112 | settings = { 113 | port = 53; 114 | 115 | log-queries = "extra"; 116 | log-debug = true; 117 | 118 | # Don't read upstream resolvers from /etc/resolv.conf 119 | no-resolv = true; 120 | 121 | # Only answer to queries from LAN 122 | local-service = true; 123 | 124 | # Never forward queries for simple names 125 | domain-needed = true; 126 | 127 | # Add domain (defined with "domain=...") to simple names in 128 | # /etc/hosts 129 | expand-hosts = true; 130 | 131 | domain = vpnDomain; 132 | local = "/${vpnDomain}/"; 133 | 134 | # NXDOMAIN reverse lookups that don't resolve to hosts in 135 | # /etc/hosts or the DHCP leases file 136 | bogus-priv = true; 137 | 138 | # Some CNAMEs; used for testing successful name resolution in 139 | # the test script 140 | cname = [ 141 | "resolver-cname,resolver-cname.${vpnDomain},resolver" 142 | "server-cname,server-cname.${vpnDomain},server" 143 | "client-cname,client-cname.${vpnDomain},client" 144 | ]; 145 | 146 | dnssec = true; 147 | trust-anchor = lib.importJSON ./trust-anchor.json; 148 | }; 149 | }; 150 | 151 | services.routedns = { 152 | enable = true; 153 | 154 | settings = { 155 | resolvers = { 156 | local-tcp = { 157 | address = "127.0.0.1:53"; 158 | protocol = "tcp"; 159 | }; 160 | 161 | local-udp = { 162 | address = "127.0.0.1:53"; 163 | protocol = "udp"; 164 | }; 165 | }; 166 | 167 | listeners = let 168 | commonConfig = { 169 | address = ":${toString resolverPort}"; 170 | 171 | # Generated with `mkcert`. 172 | server-crt = ./resolver.crt; 173 | server-key = ./resolver.key; 174 | }; 175 | in { 176 | local-dot = 177 | commonConfig 178 | // { 179 | protocol = "dot"; 180 | resolver = "local-tcp"; 181 | }; 182 | 183 | local-dtls = 184 | commonConfig 185 | // { 186 | protocol = "dtls"; 187 | resolver = "local-udp"; 188 | }; 189 | }; 190 | }; 191 | }; 192 | }; 193 | 194 | server = {nodes, ...}: let 195 | resolverIP = fetchFirstAddress { 196 | node = nodes.resolver; 197 | default = "192.168.1.2"; 198 | }; 199 | in { 200 | # NOTE -- server push settings appear to be ignored in 201 | # shared-secret/point-to-point configurations. Instead of doing 202 | # (for example): 203 | # 204 | # push "dhcp-option DNS ${resolverIP}" 205 | # push "dhcp-option DOMAIN ${vpnDomain}" 206 | # 207 | # in the server configuration, we instead put 208 | # 209 | # dhcp-option DNS ${resolverIP} 210 | # dhcp-option DOMAIN ${vpnDomain} 211 | # 212 | # in the client configuration. 213 | services.openvpn.servers.${instanceName} = { 214 | config = '' 215 | dev ${devName} 216 | port ${toString serverPort} 217 | secret ${./openvpn.key.static} 218 | ifconfig ${serverEndpoint} ${clientEndpoint} 219 | providers legacy default 220 | ''; 221 | }; 222 | 223 | networking.firewall = { 224 | trustedInterfaces = [interface]; 225 | allowedUDPPorts = [serverPort]; 226 | }; 227 | }; 228 | 229 | client = { 230 | nodes, 231 | config, 232 | lib, 233 | pkgs, 234 | ... 235 | }: let 236 | resolverIP = fetchFirstAddress { 237 | node = nodes.resolver; 238 | default = "192.168.1.2"; 239 | }; 240 | polkitRules = mkPolkitRulesForService config.systemd.services.${serviceAttrName}; 241 | in { 242 | imports = [ 243 | self.nixosModules.update-systemd-resolved 244 | ]; 245 | 246 | networking.useNetworkd = true; 247 | 248 | services.resolved = { 249 | enable = true; 250 | dnssec = "false"; # overridden for VPN interface 251 | extraConfig = '' 252 | MulticastDNS=no 253 | ''; 254 | }; 255 | 256 | users.users.openvpn = { 257 | description = "openvpn client user"; 258 | shell = "${pkgs.utillinux}/bin/nologin"; 259 | isSystemUser = true; 260 | group = "network"; 261 | }; 262 | 263 | users.groups.network = {}; 264 | 265 | # networking.useDHCP not possible when networking.useNetworkd is in 266 | # effect 267 | networking.useDHCP = false; 268 | 269 | systemd.network = { 270 | enable = true; 271 | networks.default = { 272 | # Rely on the fact that these QEMU machines use interfaces 273 | # named eth* 274 | name = "eth*"; 275 | DHCP = "yes"; 276 | }; 277 | }; 278 | 279 | environment.systemPackages = with pkgs; [ 280 | dnsutils # for "dig" 281 | ]; 282 | 283 | programs.update-systemd-resolved.servers.${instanceName} = { 284 | includeAutomatically = true; 285 | 286 | settings = { 287 | dns.resolver = {name, ...}: { 288 | address = resolverIP; 289 | port = resolverPort; 290 | sni = name; 291 | }; 292 | 293 | domain = vpnDomain; 294 | 295 | defaultRoute = true; 296 | dnsOverTLS = "yes"; 297 | dnssec = true; 298 | dnssecNegativeTrustAnchors = [vpnDomain]; 299 | flushCaches = "yes"; 300 | llmnr = "resolve"; 301 | multicastDNS = "default"; 302 | resetServerFeatures = true; 303 | resetStatistics = "yes"; 304 | }; 305 | }; 306 | 307 | services.openvpn.servers.${instanceName} = { 308 | config = '' 309 | remote server 310 | port ${toString serverPort} 311 | secret ${./openvpn.key.static} 312 | dev ${devName} 313 | ifconfig ${clientEndpoint} ${serverEndpoint} 314 | 315 | providers legacy default 316 | ''; 317 | }; 318 | 319 | # Add our generated ruleset to the system's polkit rules 320 | environment.etc."polkit-1/rules.d/10-update-systemd-resolved.rules".source = polkitRules; 321 | 322 | # `mkcert` CA 323 | security.pki.certificateFiles = [./rootCA.pem]; 324 | 325 | security.polkit = { 326 | enable = true; 327 | debug = true; 328 | 329 | # Log authorization checks. 330 | extraConfig = '' 331 | polkit.addRule(function(action, subject) { 332 | polkit.log("user " + subject.user + " is attempting action " + action.id + " from PID " + subject.pid); 333 | }); 334 | ''; 335 | }; 336 | 337 | # Override the openvpn service definition to make it run as the 338 | # "openvpn" user and "network" group. Mimic the 339 | # openvpn-client@.service from Arch Linux in other respects, too. 340 | # Additionally, make the unit wait on name resolution to be "up" by 341 | # adding "After = nss-lookup.target"; otherwise, we could hit a 342 | # race condition where the OpenVPN client service is up and we 343 | # start attempting to resolve hostnames before systemd-resolved is 344 | # fully initialized. 345 | systemd.services.${serviceAttrName} = { 346 | serviceConfig = let 347 | capabilities = '' 348 | CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE 349 | ''; 350 | in { 351 | User = "openvpn"; 352 | Group = "network"; 353 | 354 | PrivateTmp = "true"; 355 | AmbientCapabilities = capabilities; 356 | CapabilityBoundingSet = capabilities; 357 | LimitNPROC = "10"; 358 | DeviceAllow = [ 359 | "/dev/null rw" 360 | "/dev/net/tun rw" 361 | ]; 362 | ProtectSystem = "true"; 363 | ProtectHome = "true"; 364 | KillMode = "process"; 365 | }; 366 | 367 | after = lib.mkAfter ["nss-lookup.target"]; 368 | }; 369 | }; 370 | }; 371 | 372 | testScript = {nodes, ...}: let 373 | resolverIP = fetchFirstAddress { 374 | node = nodes.resolver; 375 | default = "192.168.1.2"; 376 | }; 377 | serverIP = fetchFirstAddress { 378 | node = nodes.server; 379 | default = "192.168.1.3"; 380 | }; 381 | in '' 382 | import shlex 383 | 384 | def wait_for_unit_with_output(machine, unit): 385 | try: 386 | machine.wait_for_unit(unit) 387 | except Exception as e: 388 | machine.execute('systemctl status -l {0} 1>&2'.format(unit)) 389 | raise(e) 390 | 391 | def dump_resolved_info(machine): 392 | with machine.nested('printing resolved status and statistics'): 393 | machine.succeed('resolvectl status 1>&2') 394 | machine.succeed('resolvectl statistics 1>&2') 395 | 396 | def assert_hostname_match(machine, expected, *args): 397 | cmd = shlex.join(['dig', '+short', *args]) 398 | 399 | # Even after waiting for nss-lookup.target, lookups can still fail for 400 | # reasons unrelated to any update-systemd-resolved misbehaviour. Retry 401 | # name resolution in order to work around race conditions/other issues. 402 | def hostname_matches(_): 403 | status, output = machine.execute(cmd) 404 | 405 | if status != 0: 406 | machine.log('expected query to succeed, but `dig` exited with {0}'.format(status)) 407 | return False 408 | 409 | for line in output.splitlines(): 410 | if line == expected: 411 | return True 412 | 413 | machine.log('expected query to resolve to address "{0}", but got: {1}'.format(expected, output)) 414 | return False 415 | 416 | with machine.nested('checking that hostname resolves to expected address "{0}" from {1}'.format(expected, machine.name)): 417 | retry(hostname_matches) 418 | 419 | def extract_interface_property(machine, interface, property, *args): 420 | with machine.nested('extracting property "{0}" of interface "{1}"'.format(property, interface)): 421 | cmd = shlex.join(['resolvectl', *args, property, interface]) 422 | return machine.succeed("{0} | grep -m1 -Po '(?<=:\s).*'".format(cmd)).rstrip() 423 | 424 | def assert_interface_property(machine, interface, property, expected, *args): 425 | def interface_property_matches(_): 426 | actual = extract_interface_property(machine, interface, property, *args) 427 | machine.log('property "{0}" of interface "{1}" is "{2}"'.format(property, interface, actual)) 428 | if actual == expected: 429 | return True 430 | else: 431 | machine.log('expected property "{0}" of interface "{1}" to be "{2}", but got "{3}"'.format(property, interface, expected, actual)) 432 | return False 433 | 434 | with machine.nested('checking that property "{0}" of interface "{1}" is "{2}"'.format(property, interface, expected)): 435 | retry(interface_property_matches) 436 | 437 | # Machine.wait_for_open_port only checks ports on localhost 438 | def wait_for_open_host_port(machine, host, port, extra=[]): 439 | cmd = shlex.join(['nc'] + extra + ['-z', host, str(port)]) 440 | 441 | # `retry` passes an argument to the provided function 442 | def host_port_is_open(_): 443 | status, _ = machine.execute(cmd) 444 | return status == 0 445 | 446 | with machine.nested('checking that host and port "{0}:{1}" are open from the perspective of {2}'.format(host, port, machine.name)): 447 | retry(host_port_is_open) 448 | 449 | client.succeed('cat /etc/polkit-1/rules.d/10-update-systemd-resolved.rules 1>&2') 450 | client.succeed('systemctl cat ${serviceName} 1>&2') 451 | 452 | resolver.start() 453 | wait_for_unit_with_output(resolver, 'dnsmasq') 454 | wait_for_unit_with_output(resolver, 'routedns') 455 | 456 | server.start() 457 | wait_for_unit_with_output(server, '${serviceName}') 458 | 459 | client.start() 460 | 461 | wait_for_unit_with_output(client, '${serviceName}') 462 | 463 | # Block until we can reach the resolver (or until we hit the retry 464 | # timeout). Pass `-u` flag to check UDP port; also check TCP port. 465 | wait_for_open_host_port(client, '${resolverIP}', ${toString resolverPort}, extra=['-u']) 466 | wait_for_open_host_port(client, '${resolverIP}', ${toString resolverPort}) 467 | 468 | with subtest('interface info before attempting to resolve names'): 469 | assert_interface_property(client, '${interface}', 'default-route', 'yes') 470 | assert_interface_property(client, '${interface}', 'llmnr', 'resolve') 471 | assert_interface_property(client, '${interface}', 'mdns', 'no') 472 | assert_interface_property(client, '${interface}', 'dnsovertls', 'yes') 473 | assert_interface_property(client, '${interface}', 'dnssec', 'yes') 474 | 475 | with subtest('resolved info before attempting to resolve names'): 476 | dump_resolved_info(client) 477 | 478 | with subtest('attempt to resolve names using settings from OpenVPN'): 479 | assert_hostname_match(client, '${resolverIP}', 'resolver-cname.${vpnDomain}') 480 | assert_hostname_match(client, '${serverIP}', 'server-cname.${vpnDomain}') 481 | 482 | with subtest('interface info after attempting to resolve names'): 483 | assert_interface_property(client, '${interface}', 'default-route', 'yes') 484 | assert_interface_property(client, '${interface}', 'llmnr', 'resolve') 485 | assert_interface_property(client, '${interface}', 'mdns', 'no') 486 | assert_interface_property(client, '${interface}', 'dnsovertls', 'yes') 487 | assert_interface_property(client, '${interface}', 'dnssec', 'yes') 488 | 489 | with subtest('resolved info after attempting to resolve names'): 490 | dump_resolved_info(client) 491 | 492 | client.succeed('systemctl restart ${serviceName}') 493 | wait_for_unit_with_output(client, '${serviceName}') 494 | 495 | dump_resolved_info(client) 496 | ''; 497 | }; 498 | }; 499 | } 500 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # update-systemd-resolved 2 | 3 | [![Build Status](https://github.com/jonathanio/update-systemd-resolved/actions/workflows/test.yml/badge.svg)](https://github.com/jonathanio/update-systemd-resolved/actions) 4 | 5 | This is a helper script designed to integrate OpenVPN with the 6 | `systemd-resolved` service via DBus instead of trying to override 7 | `/etc/resolv.conf`, or manipulate `systemd-networkd` configuration files. 8 | 9 | Since systemd-229, the `systemd-resolved` service has an API available via DBus 10 | which allows directly setting the DNS configuration for a link. This script 11 | makes use of `busctl` from systemd to send DBus messages to `systemd-resolved` 12 | to update the DNS for the link created by OpenVPN. 13 | 14 | ## Prerequisites 15 | 16 | This script requires: 17 | 18 | - Bash 4.3 or above. 19 | - [coreutils](https://www.gnu.org/software/coreutils/) or 20 | [busybox](https://www.busybox.net/) (for the `id` command). 21 | - [iproute2](https://wiki.linuxfoundation.org/networking/iproute2) (for the 22 | `ip` command). 23 | - [systemd](https://systemd.io/) (for the `busctl` and `resolvectl` commands). 24 | 25 | ### Optional dependencies: 26 | 27 | #### IP Parsing and Validation 28 | 29 | - [`python`](https://python.org), **or** 30 | - [`sipcalc`](https://github.com/sii/sipcalc). 31 | 32 | If available, these will be used for IP address parsing and 33 | validation;[^iphandling] otherwise `update-systemd-resolved` will use native 34 | Bash routines for this. 35 | 36 | [^iphandling]: Required for translating numerical labels like `1.2.3.4` to the 37 | byte arrays recognized by [the `SetLinkDNS()` function on 38 | `systemd-resolved`'s `org.freedesktop.resolve1.Manager` D-Bus 39 | interface][resolved]). 40 | 41 | #### Logging 42 | 43 | - [util-linux](https://en.wikipedia.org/wiki/Util-linux) 44 | 45 | If available, the `logger` command included in the `util-linux` distribution 46 | will be used for logging. Otherwise, all logs will go to standard error using 47 | Bash's `printf` builtin. 48 | 49 | #### Polkit Rules Generation 50 | 51 | - [`jq`](https://jqlang.github.io/jq/), **or** 52 | - [`perl`](https://www.perl.org/), **or** 53 | - [`python`](https://python.org). 54 | 55 | If available, these will be used for serializing the [names of the users and 56 | groups allowed to call `systemd-resolved`'s DBus methods](#polkit-rules) to 57 | JSON lists for use within the [generated polkit 58 | rules](#generating-polkit-rules). Otherwise, `update-systemd-resolved` will 59 | fall back to native Bash routines for generating these lists. 60 | 61 | ## Installation 62 | 63 | [aur]:https://aur.archlinux.org/packages/openvpn-update-systemd-resolved/ 64 | 65 | If you are using a distribution of Linux with uses the Arch User Repository, the 66 | simplest way to install is by using the [openvpn-update-systemd-resolved][aur] 67 | AUR package as this will take care of any updates through your package manager. 68 | [Debian](https://packages.debian.org/openvpn-systemd-resolved) and 69 | [Ubuntu](https://packages.ubuntu.com/openvpn-systemd-resolved) also provide a 70 | `.deb` package in their distributions. 71 | 72 | Alternatively, the package can be manually installed by running the following: 73 | 74 | ```bash 75 | git clone https://github.com/jonathanio/update-systemd-resolved.git 76 | cd update-systemd-resolved 77 | make 78 | ``` 79 | 80 | ### Nix and NixOS 81 | 82 | [Nix flake]:https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html 83 | 84 | `update-systemd-resolved` exposes a [Nix flake][]. You can incorporate this 85 | flake into your flake by adding it to your inputs: 86 | 87 | ```nix 88 | # Your flake.nix 89 | { 90 | inputs = { 91 | # Other inputs here... 92 | 93 | update-systemd-resolved.url = "github:jonathanio/update-systemd-resolved"; 94 | update-systemd-resolved.inputs.nixpkgs.follows = "nixpkgs"; # optional 95 | }; 96 | 97 | # Etc. 98 | } 99 | ``` 100 | 101 | This flake provides the `update-systemd-resolved` package for several Linux 102 | architectures. It also provides the `update-systemd-resolved` NixOS module: 103 | 104 | ```nix 105 | # Your flake.nix 106 | { 107 | outputs = {nixpkgs, update-systemd-resolved, ...}: { 108 | nixosConfigurations.my-system = nixpkgs.lib.nixosSystem { 109 | system = "x86_64-linux"; 110 | modules = [ 111 | update-systemd-resolved.nixosModules.update-systemd-resolved 112 | ]; 113 | }; 114 | }; 115 | } 116 | ``` 117 | 118 | Please see [the NixOS module documentation](/docs/nixos-modules.md) for 119 | available options. 120 | 121 | To view all outputs provided by this flake, run the following command: 122 | 123 | ```shell-session 124 | $ nix flake show 'github:jonathanio/update-systemd-resolved' 125 | ``` 126 | 127 | ## How to Enable 128 | 129 | Make sure that you have `systemd-resolved` enabled and running. First, make sure 130 | that `systemd-resolved.service` is enabled and started: 131 | 132 | ```bash 133 | systemctl enable systemd-resolved.service 134 | systemctl start systemd-resolved.service 135 | ``` 136 | 137 | Next, you can either configure the system libraries to talk to it using NSS, or 138 | you can override the `resolv.conf` file to use `systemd-resolved` as a stub 139 | resolver (or both): 140 | 141 | ### NSS and nssswitch.conf 142 | 143 | Update your `/etc/nsswitch.conf` file to look up DNS via the `resolve` service 144 | (you may need to install the NSS library which connects libnss to 145 | `systemd-resolved`): 146 | 147 | ```conf 148 | # Use /etc/resolv.conf first, then fall back to systemd-resolved 149 | hosts: files dns resolve myhostname 150 | # Use systemd-resolved first, then fall back to /etc/resolv.conf 151 | hosts: files resolve dns myhostname 152 | # Don't use /etc/resolv.conf at all 153 | hosts: files resolve myhostname 154 | ``` 155 | 156 | The changes will be applied as soon as the file is saved. 157 | 158 | Note that [some Linux distributions manage `/etc/nsswitch.conf`](#fedora), so 159 | manual edits to `/etc/nsswitch.conf` may disappear. Please consult your 160 | distribution's documentation for how to configure `/etc/nsswitch.conf`. 161 | 162 | ### Polkit Rules 163 | 164 | If you run the OpenVPN client as an unprivileged user, you may need to add 165 | polkit rules authorizing that user to perform the various DBus calls that 166 | `update-systemd-resolved` makes. Some installation methods bundle these rules; 167 | for instance, on Arch Linux, where `openvpn-client@.service` instances 168 | run as the unprivileged `openvpn` user, the 169 | [openvpn-update-systemd-resolved][aur] AUR package ships suitable rules in the 170 | file `/etc/polkit-1/rules.d/10-update-systemd-resolved.rules`. 171 | 172 | #### Generating Polkit Rules 173 | 174 | > [!WARNING] 175 | > `update-systemd-resolved` strives to generate polkit rules with the smallest 176 | > scope consistent with its proper functioning. Nonetheless, in order to avoid 177 | > security risks, you are encouraged to review the generated polkit rules 178 | > before installing them. 179 | 180 | You can also generate suitable rules with (some variation on) the following 181 | commands: 182 | 183 | ```shell-session 184 | $ update-systemd-resolved print-polkit-rules --polkit-allowed-user some-user --polkit-allowed-user another-user > ./10-custom-update-systemd-resolved.rules 185 | $ sudo install -Dm0640 ./10-custom-update-systemd-resolved.rules /etc/polkit-1/rules.d/10-custom-update-systemd-resolved.rules 186 | ``` 187 | 188 | This will allow `update-systemd-resolved` to successfully make its DBus calls 189 | when invoked from OpenVPN client services that run as the users `some-user` or 190 | `another-user`. 191 | 192 | You can also authorize members of specified groups with: 193 | 194 | ```shell-session 195 | $ update-systemd-resolved print-polkit-rules --polkit-allowed-group some-group --polkit-allowed-group another-group > ./10-custom-update-systemd-resolved.rules 196 | $ sudo install -Dm0640 ./10-custom-update-systemd-resolved.rules /etc/polkit-1/rules.d/10-custom-update-systemd-resolved.rules 197 | ``` 198 | 199 | This will allow `update-systemd-resolved` to successfully make its DBus calls 200 | when invoked from OpenVPN client services that run under the groups 201 | `some-group` or `another-group`. 202 | 203 | Finally, you can generate rules that pull appropriate user and group values 204 | from OpenVPN systemd units with: 205 | 206 | ```shell-session 207 | $ update-systemd-resolved print-polkit-rules --polkit-systemd-openvpn-unit my-openvpn-client.service 208 | $ sudo install -Dm0640 ./10-custom-update-systemd-resolved.rules /etc/polkit-1/rules.d/10-custom-update-systemd-resolved.rules 209 | ``` 210 | 211 | Given: 212 | 213 | ```shell-session 214 | $ systemctl show -P User my-openvpn-client.service 215 | myuser 216 | $ systemctl show -P Group my-openvpn-client.service 217 | mygroup 218 | ``` 219 | 220 | The generated `10-custom-update-systemd-resolved.rules` file will contain rules 221 | allowing the `myuser` user and members of the `mygroup` group to perform the 222 | requisite DBus calls. 223 | 224 | You can run `update-systemd-resolved print-polkit-rules` with any combination 225 | of `--polkit-allowed-user`, `--polkit-allowed-group`, and 226 | `--polkit-systemd-openvpn-unit`. If called without options, 227 | `update-systemd-resolved print-polkit-rules` will attempt to derive appropriate 228 | user and group authorizations from a systemd OpenVPN unit matching 229 | `openvpn-client@.service`, the [systemd service 230 | template](https://www.freedesktop.org/software/systemd/man/systemd.service.html#Service%20Templates) 231 | used for OpenVPN client services on distributions including Arch Linux. 232 | 233 | ### Stub Resolver 234 | 235 | The `systemd-resolved` service (since systemd-231) also listens on `127.0.0.53` 236 | via the `lo` interface, providing a stub resolver which any client can call to 237 | request DNS, whether or not it uses the system libraries to resolve DNS, and 238 | you no longer have to worry about trying to manage your `/etc/resolv.conf` 239 | file. This set up can be installed by linking to `stub-resolv.conf`: 240 | 241 | ```bash 242 | ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf 243 | ``` 244 | 245 | ### OpenVPN Configuration 246 | 247 | Finally, update your OpenVPN configuration file and set the `up` and `down` 248 | options to point to the script, and `down-pre` to ensure that the script is run 249 | before the device is closed: 250 | 251 | ```conf 252 | script-security 2 253 | up /usr/local/libexec/openvpn/update-systemd-resolved 254 | up-restart 255 | down /usr/local/libexec/openvpn/update-systemd-resolved 256 | down-pre 257 | 258 | # If needed, to permit `update-systemd-resolved` to find utilities it depends 259 | # on. Adjust to suit your system. 260 | #setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 261 | ``` 262 | 263 | #### up-restart 264 | 265 | It is recommended to use `up-restart` in your configuration to ensure that 266 | `upate-systemd-resolved` is run on restarts - where the connection is 267 | re-established but the TUN/TAP device remained open (for example, where the 268 | original connection has timed out and `persist-tun` is enabled). If you do not 269 | have `persist-tun` set, or you use `ping-exit` instead of `ping-timeout`, you 270 | most likely will not need this. 271 | 272 | #### down/pre-down with user/group 273 | 274 | The `down` and `down-pre` options here may not work as expected where the 275 | `openvpn` daemon drops privileges after establishing the connection (i.e. when 276 | using the `user` and `group` options). This is because, by default, only the 277 | `root` user will have the privileges required to talk to 278 | `systemd-resolved.service` over DBus. The `openvpn-plugin-down-root.so` plug-in 279 | does provide support for enabling the `down` script to be run as the `root` 280 | user, but this has been known to be unreliable. 281 | 282 | You can authorize unprivileged users or groups to revert the OpenVPN link's DNS 283 | settings during the "down" phase using the methods described in the ["Polkit 284 | Rules" section](#polkit-rules). 285 | 286 | Ultimately, dropping privileges shouldn't affect normal "down" operation, since 287 | `systemd-resolved.service` will remove all settings associated with the link 288 | (and therefore naturally update `/etc/resolv.conf`, if you have it symlinked) 289 | when the TUN or TAP device is closed. The option for `down` and `down-pre` just 290 | make this step explicit before the device is torn down rather than implicit on 291 | the change in environment. 292 | 293 | ### Command Line Settings 294 | 295 | Alternatively if you don't want to edit your client configuration, you can add 296 | the following options to your `openvpn` command: 297 | 298 | ```bash 299 | openvpn \ 300 | --script-security 2 \ 301 | --setenv PATH '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ 302 | --up /usr/local/libexec/openvpn/update-systemd-resolved --up-restart \ 303 | --down /usr/local/libexec/openvpn/update-systemd-resolved --down-pre 304 | ``` 305 | 306 | > [!TIP] 307 | > The `--setenv PATH` option shown above is intended to allow 308 | > `update-systemd-resolved` to find [its prerequisites](#prerequisites). 309 | > Depending on your system's configuration, you may not need `--setenv PATH`, 310 | > or you may need to specify a different `PATH` value than the one shown above. 311 | 312 | Or, you can add the following argument to the command-line arguments of 313 | `openvpn`, which will use the `update-systemd-resolve.conf` file instead: 314 | 315 | ```bash 316 | openvpn --config /usr/local/share/doc/openvpn/update-systemd-resolved.conf 317 | ``` 318 | 319 | > [!NOTE] 320 | > The path to `update-systemd-resolved.conf` may differ depending on how you 321 | > installed `update-systemd-resolved`. Additionally, both the file's path and 322 | > its contents are subject to change in future releases. Rather than using the 323 | > example configuration file directory, you may want to copy the file to 324 | > another location and then run `openvpn --config /update-systemd-resolved.conf`. 325 | 326 | ## :screwdriver: Usage :wrench: 327 | 328 | `update-systemd-resolved` works by processing the `dhcp-option` commands set in 329 | OpenVPN, either through the server, or the client, configuration. **Note** 330 | that there are no local or system options to be configured. All configuration 331 | for this script is handled through OpenVPN, including, for example, the name of 332 | the interface to be configured. 333 | 334 | ### :level_slider: Options :control_knobs: 335 | 336 | [resolved]:https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html 337 | 338 | #### :gear: `DNS` 339 | 340 |
341 | 342 | Setting DNS servers 343 | 344 | ##### Examples 345 | 346 | - `0.0.0.0` 347 | - `0.0.0.0:5353` 348 | - `0.0.0.0#my.resolver.net` 349 | - `0.0.0.0:5353#my.resolver.net` 350 | - `::1` 351 | - `[::1]:5353` 352 | - `::1#my.resolver.net` 353 | - `[::1]:5353#my.resolver.net` 354 | 355 | ##### Description 356 | 357 | This sets the DNS servers for the link and can take any IPv4 or IPv6 address. 358 | 359 | ##### DBus call 360 | 361 | [SetLinkDNS][resolved], [SetLinkDNSEx][resolved] 362 | 363 |
364 | 365 | #### :gear: `DNS6` 366 | 367 |
368 | 369 | Setting IPv6-only DNS servers 370 | 371 | ##### Examples 372 | 373 | - `::1` 374 | - `[::1]:5353` 375 | - `::1#my.resolver.net` 376 | - `[::1]:5353#my.resolver.net` 377 | 378 | ##### Description 379 | 380 | This sets the DNS servers for the link and can take only IPv6 addresses. 381 | 382 | ##### DBus call 383 | 384 | [SetLinkDNS][resolved], [SetLinkDNSEx][resolved] 385 | 386 |
387 | 388 | #### :gear: `DOMAIN` or `ADAPTER_DOMAIN_SUFFIX` 389 | 390 |
391 | 392 | Setting the primary domain 393 | 394 | ##### Examples 395 | 396 | - `example.com` 397 | 398 | ##### Description 399 | 400 | The primary domain for this host. If set multiple times, the first provided is 401 | used as the primary search domain for bare hostnames. Any subsequent `DOMAIN` 402 | options will be added as the equivalent of `DOMAIN-SEARCH` options. All 403 | requests for this domain as well will be routed to the `DNS` servers provided 404 | on this link. 405 | 406 | ##### DBus call 407 | 408 | [SetLinkDomains][resolved] 409 | 410 |
411 | 412 | #### :gear: `DOMAIN-SEARCH` 413 | 414 |
415 | 416 | Setting secondary domains 417 | 418 | ##### Examples 419 | 420 | - `example.com` 421 | 422 | ##### Description 423 | 424 | Secondary domains which will be used to search for bare hostnames (after any 425 | `DOMAIN`, if set) and in the order provided. All requests for this domain will 426 | be routed to the `DNS` servers provided on this link. 427 | 428 | ##### DBus call 429 | 430 | [SetLinkDomains][resolved] 431 | 432 |
433 | 434 | #### :gear: `DOMAIN-ROUTE` 435 | 436 |
437 | 438 | Routing DNS queries 439 | 440 | ##### Examples 441 | 442 | - `example.com` 443 | 444 | ##### Description 445 | 446 | All requests for these domains will be routed to the `DNS` servers provided on 447 | this link. They will *not* be used to search for bare hostnames, only routed. A 448 | `DOMAIN-ROUTE` option for `.` (single period) will instruct `systemd-resolved` 449 | to route the entire DNS name-space through to the `DNS` servers configured for 450 | this connection (unless a more specific route has been offered by another 451 | connection for a selected name/name-space). This is useful if you wish to 452 | prevent [DNS leakage](#dns-leakage). 453 | 454 | ##### DBus call 455 | 456 | [SetLinkDomains][resolved] 457 | 458 |
459 | 460 | #### :gear: `DNSSEC` 461 | 462 |
463 | 464 | Enabling DNSSEC 465 | 466 | ##### Examples 467 | 468 | - `yes`, `true` 469 | - `no`, `false` 470 | - `default` 471 | - `allow-downgrade` 472 | 473 | ##### Description 474 | 475 | Control of DNSSEC should be enabled (`yes`, `true`) or disabled (`no`, 476 | `false`), or `allow-downgrade` to switch off DNSSEC only if the server doesn't 477 | support it, for any queries over this link only, or use the system default 478 | (`default`). 479 | 480 | ##### DBus call 481 | 482 | [DNSSEC][resolved] 483 | 484 |
485 | 486 | #### :gear: `FLUSH-CACHES` 487 | 488 |
489 | 490 | Flushing DNS caches 491 | 492 | ##### Examples 493 | 494 | - `yes`, `true` 495 | - `no`, `false` 496 | 497 | ##### Description 498 | 499 | Whether or not to flush all local DNS caches. Enabled by default. 500 | 501 | ##### DBus call 502 | 503 | [FlushCaches][resolved] 504 | 505 |
506 | 507 | #### :gear: `RESET-SERVER-FEATURES` 508 | 509 |
510 | 511 | Resetting learnt DNS server feature levels 512 | 513 | ##### Examples 514 | 515 | - `yes`, `true` 516 | - `no`, `false` 517 | 518 | ##### Description 519 | 520 | Whether or not to forget learnt DNS server feature levels. 521 | 522 | ##### DBus call 523 | 524 | [ResetServerFeatures][resolved] 525 | 526 |
527 | 528 | #### :gear: `RESET-STATISTICS` 529 | 530 |
531 | 532 | Resetting resolver statistics 533 | 534 | ##### Examples 535 | 536 | - `yes`, `true` 537 | - `no`, `false` 538 | 539 | ##### Description 540 | 541 | Whether or not to reset resolver statistics. 542 | 543 | ##### DBus call 544 | 545 | [ResetStatistics][resolved] 546 | 547 |
548 | 549 | #### :gear: `DEFAULT-ROUTE` 550 | 551 |
552 | 553 | Default DNS query routing 554 | 555 | ##### Examples 556 | 557 | - `yes`, `true` 558 | - `no`, `false` 559 | 560 | ##### Description 561 | 562 | If true, this link's configured DNS servers are used for resolving domain names 563 | that do not match any link's configured `Domains=` setting. If false, this 564 | link's configured DNS servers are never used for such domains, and are 565 | exclusively used for resolving names that match at least one of the domains 566 | configured on this link. 567 | 568 | ##### DBus call 569 | 570 | [DNSDefaultRoute][resolved] 571 | 572 |
573 | 574 | #### :gear: `DNS-OVER-TLS` 575 | 576 |
577 | 578 | Enabling DNS-over-TLS 579 | 580 | ##### Examples 581 | 582 | - `yes`, `true` 583 | - `no`, `false` • `opportunistic` • `default` 584 | 585 | ##### Description 586 | 587 | If true all connections to the server will be encrypted. Note that this mode 588 | requires a DNS server that supports DNS-over-TLS and has a valid certificate. 589 | If the hostname was specified in `DNS=` by using the format 590 | `address#server_name` it is used to validate its certificate and also to enable 591 | Server Name Indication (SNI) when opening a TLS connection. Otherwise the 592 | certificate is checked against the server's IP. If the DNS server does not 593 | support DNS-over-TLS all DNS requests will fail. When set to `opportunistic` 594 | DNS request are attempted to send encrypted with DNS-over-TLS. If the DNS 595 | server does not support TLS, DNS-over-TLS is disabled. Note that this mode 596 | makes DNS-over-TLS vulnerable to "downgrade" attacks, where an attacker might 597 | be able to trigger a downgrade to non-encrypted mode by synthesizing a response 598 | that suggests DNS-over-TLS was not supported. If set to false, DNS lookups are 599 | send over UDP. If set to `default`, uses the system default. 600 | 601 | ##### DBus call 602 | 603 | [SetLinkDNSOverTLS][resolved] 604 | 605 |
606 | 607 | #### :gear: `LLMNR` 608 | 609 |
610 | 611 | Enabling Link-Local Multicast Name Resolution 612 | 613 | ##### Examples 614 | 615 | - `yes`, `true` 616 | - `no`, `false` • `resolve` • `default` 617 | 618 | ##### Description 619 | 620 | When true, enables Link-Local Multicast Name Resolution on the link. When set 621 | to `resolve`, only resolution is enabled, but not host registration and 622 | announcement. If set to `default`, uses the system default. 623 | 624 | ##### DBus call 625 | 626 | [SetLinkLLMNR][resolved] 627 | 628 |
629 | 630 | #### :gear: `MULTICAST-DNS` 631 | 632 |
633 | 634 | Enabling Multicast DNS 635 | 636 | ##### Examples 637 | 638 | - `yes`, `true` 639 | - `no`, `false` • `resolve` • `default` 640 | 641 | ##### Description 642 | 643 | When true, enables Multicast DNS support on the link. When set to `resolve`, 644 | only resolution is enabled, but not host or service registration and 645 | announcement. If set to `default`, uses the system default. 646 | 647 | ##### DBus call 648 | 649 | [SetLinkMulticastDNS][resolved] 650 | 651 |
652 | 653 | #### :gear: `DNSSEC-NEGATIVE-TRUST-ANCHORS` 654 | 655 |
656 | 657 | Configuring DNSSEC Negative Trust Anchors 658 | 659 | ##### Examples 660 | 661 | - `trusted.org` 662 | 663 | ##### Description 664 | 665 | If specified and DNSSEC is enabled, look-ups done via the interface's DNS 666 | server will be subject to the list of negative trust anchors, and not require 667 | authentication for the specified domains, or anything below it. Use this to 668 | disable DNSSEC authentication for specific private domains, that cannot be 669 | proven valid using the Internet DNS hierarchy. By default, 670 | `update-systemd-resolved` does not set any negative trust anchors. 671 | 672 | ##### DBus call 673 | 674 | [SetLinkDNSSECNegativeTrustAnchors][resolved] 675 | 676 |
677 | 678 | ### Example 679 | 680 | ```conf 681 | push "dhcp-option DNS 10.62.3.2" 682 | push "dhcp-option DNS 10.62.3.3" 683 | push "dhcp-option DNS6 2001:db8::a3:c15c:b56e:619a" 684 | push "dhcp-option DNS6 2001:db8::a3:ffec:f61c:2e06" 685 | push "dhcp-option DOMAIN example.office" 686 | push "dhcp-option DOMAIN example.lan" 687 | push "dhcp-option DOMAIN-SEARCH example.com" 688 | push "dhcp-option DOMAIN-ROUTE example.net" 689 | push "dhcp-option DOMAIN-ROUTE example.org" 690 | push "dhcp-option DNSSEC yes" 691 | ``` 692 | 693 | This, added to the OpenVPN server's configuration file will set two IPv4 DNS 694 | servers and two IPv6 and will set the primary domain for the link to be 695 | `example.office`. Therefore if you try to look up the bare address `mail` then 696 | `mail.example.office` will be attempted first. The domains `example.lan` and 697 | `example.com` are also added as an additional search domain, so if 698 | `mail.example.office` fails, then `mail.example.lan` will be tried next, 699 | followed by `mail.example.com`. 700 | 701 | Requests for `example.net` and `example.org` will also be routed through to the 702 | four DNS servers listed, but they will *not* be appended (i.e. 703 | `mail.example.net` will not be attempted, nor `mail.example.org`, if 704 | `mail.example.office` or `mail.example.com` do not exist). 705 | 706 | Finally, DNSSEC has been enabled for this link (and this link only). 707 | 708 | ## DNS Leakage 709 | 710 | [resolved-vpns]: https://systemd.io/RESOLVED-VPNS 711 | 712 | > [!IMPORTANT] 713 | > Required reading: [`systemd-resolved.service` and VPNs][resolved-vpns]. This 714 | > document includes, among other things, an overview of search domains, routing 715 | > domains, and `systemd-resolved`'s `default-route` boolean settings. 716 | > Understanding these concepts will help you configure your local 717 | > `systemd-resolved` instance to ensure that DNS queries go where you want them 718 | > to go. 719 | 720 | DNS Leakage is something to be careful of when using any VPN or untrusted 721 | network, and it can heavily depend on how you configure your normal DNS 722 | settings as well as how you configure the DNS on your VPN connection. 723 | 724 | By default, `systemd-resolved` will send **all** DNS queries to at least one 725 | DNS server on **every** link configured with DNS servers. The first to reply 726 | back with a valid query is the one returned to the client, and the last to 727 | return back a failure (assuming all other queries also failed) will also be 728 | returned to the client. 729 | 730 | The changes in this handling come in when you start using the `DOMAIN`, 731 | `DOMAIN-SEARCH` and `DOMAIN-ROUTE` options. The three differ in how domains 732 | are treated for searching bare domains, but all three work exactly the same 733 | when it comes to how it routes domains to specific DNS servers. 734 | 735 | Any domain added using `DOMAIN`, `DOMAIN-SEARCH`, or `DOMAIN-ROUTE` will be 736 | added explicitly to the VPN link and therefore any queries for domain suffixes 737 | which match these will be routed through this link, and only this link. Any 738 | other domains which do not match these will revert back to distributing the 739 | queries across all links. 740 | 741 | There are two ways to override this: 742 | 743 | ### Preventing Leakage in on untrusted networks 744 | 745 | If you want to prevent DNS queries leaking over untrusted networks (for 746 | example, over public WiFi hotspots), then you need to tell `systemd-resolved` 747 | to send **all** DNS queries over the VPN link. To do this, add the following to 748 | your server or client VPN configurations respectively: 749 | 750 | ``` 751 | # Server Configuration 752 | push "dhcp-option DOMAIN-ROUTE ." 753 | ``` 754 | 755 | ``` 756 | # Client Configuration 757 | dhcp-option DOMAIN-ROUTE . 758 | ``` 759 | 760 | All DNS queries (which do not match a more explicit entry on another link) will 761 | now be routed over the VPN only. 762 | 763 | ### Preventing Leakage to Corporate networks 764 | 765 | In an alternate situation, you may want to have DNS queries specifically routed 766 | over the VPN for corporate or private network access, but you don't want your 767 | general DNS queries to be visible to anyone who has access to the logs of the 768 | corporate DNS servers. 769 | 770 | This option cannot be directly managed by `update-systemd-resolved` as you need 771 | to configure the network settings of other links to send all queries by default 772 | to your nominated DNS server (e.g. over `ens0` or `wlp2s0` for your Ethernet or 773 | Wireless network cards). This needs to be configured under the `[Network]` 774 | section of your `.network` file for your interface in `/etc/systemd/network`. 775 | For example: 776 | 777 | ``` 778 | [Network] 779 | DHCP=yes 780 | DNS=8.8.8.8 781 | DNS=8.8.4.4 782 | Domains=. 783 | ``` 784 | 785 | When you connect, all domains except those explicitly listed using the `DOMAIN`, 786 | `DOMAIN-SEARCH`, or `DOMAIN-ROUTE` options of your VPN link will be sent to the 787 | DNS server of your nominated link. 788 | 789 | ### Concurrent Configuration 790 | 791 | Note that these two options are mutually exclusive, as if you establish a VPN 792 | link with `DOMAIN-ROUTE` set to `.` while you have also configured it inside a 793 | `.network` file via `systemd-networkd`, then you will have two links 794 | responsible for routing all queries, and so both links will get all requests. 795 | 796 | How to manage the DNS settings of other links while the VPN is operational is 797 | outside the scope of this script at this time. 798 | 799 | ## Known Issues 800 | 801 | There are a number of known issues relating to some third-party servers and 802 | services: 803 | 804 | ### NetworkManager 805 | 806 | #### Compatibility with this script 807 | 808 | [nm-helper]:https://git.launchpad.net/ubuntu/+source/network-manager-openvpn/tree/src/nm-openvpn-service-openvpn-helper.c?h=debian/sid 809 | 810 | This script may not be compatible with certain versions of NetworkManager. It 811 | seems that NetworkManager overrides the `up` command to use its own helper 812 | script ([nm-openvpn-service-openvpn-helper][nm-helper]). The script that ships 813 | with NetworkManager only supports `DNS` and `DOMAIN` options (not `DNS6`, 814 | `DOMAIN-SEARCH` and `DOMAIN-ROUTE`, nor `DNSSEC` overrides). It may also be 815 | liable to set the other network interfaces to route `~.` DNS queries (i.e the 816 | whole name-space) to the LAN or ISP DNS servers, making it difficult to 817 | override using `DOMAIN` - see [the DNS leakage section](#managed-interface-dns-leakage). 818 | 819 | #### Managed interface DNS leakage 820 | 821 | [LP1671606]:https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1671606 822 | [LP1688018]:https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1688018 823 | 824 | There is a regression with versions of NetworkManager 1.2.6 through 1.26.4 (see 825 | [LP#1671606][LP1671606] and [LP#1688018][LP1688018]) which means that it will 826 | automatically set all normal network interfaces with `~.` for DNS routing. 827 | This means that even if you set `dhcp-option DOMAIN-ROUTE .` for your VPN 828 | connection, you will still leak DNS queries over potentially insecure networks. 829 | 830 | [issue-59]:https://github.com/jonathanio/update-systemd-resolved/issues/59 831 | 832 | If you are concerned by potentially leaking DNS on systems which use 833 | NetworkManager, you may need to configure an [additional script][issue-59] 834 | into NetworkManager which change the domain routing settings on all non-VPN 835 | interfaces. 836 | 837 | [fix-1.26.6]:https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/nm-1-26/NEWS#L23-24 838 | 839 | This issue was [fixed in NetworkManager version 1.26.6][fix-1.26.6]; now, 840 | NetworkManager only enables the `DefaultRoute` option on managed interfaces. 841 | 842 | ### DNSSEC Issues 843 | 844 | ```shell 845 | $ resolvectl query eu-central-1.console.aws.amazon.com 846 | eu-central-1.console.aws.amazon.com: resolve call failed: DNSSEC validation failed: no-signature 847 | # or 848 | $ resolvectl query eu-central-1.console.aws.amazon.com 849 | eu-central-1.console.aws.amazon.com: resolve call failed: DNSSEC validation failed: incompatible-server 850 | ``` 851 | 852 | If you are seeing failed queries in your logs due to DNSSEC issues, support may be 853 | partially or fully enabled and you are now working with a server which does not 854 | support this extension. You may therefore need to set `DNSSEC` to `no` (or 855 | maybe just `allow-downgrade`) in your VPN configuration. 856 | 857 | ``` 858 | dhcp-option DNSSEC allow-downgrade 859 | ``` 860 | 861 | ### Issues with Ubuntu and Fedora 862 | 863 | #### Ubuntu 864 | 865 | [LP1685045]:https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1685045 866 | 867 | The NSS interface for `systemd-resolved` may be deprecated and has already been 868 | flagged for deprecation in Ubuntu (see [LP#1685045][LP1685045] for details). In 869 | this case, you should use the [Stub Resolver](#stub-resolver) method now. 870 | 871 | #### Fedora 872 | 873 | [authselect]:https://github.com/authselect/authselect 874 | 875 | Fedora 28 makes use of `authselect` to manage the NSS settings on the system. 876 | Directly editing `nsswitch.conf` is not recommended as it may be overwritten at 877 | any time if `authselect` is run. Proper overrides may not yet be possible - see 878 | [the authselect project repository][authselect] for details. However, like 879 | Ubuntu, the [Stub Resolver](#stub-resolver) method is recommended here too. 880 | 881 | [f33-changes-systemd-resolved]:https://fedoraproject.org/wiki/Changes/systemd-resolved 882 | 883 | Note that Fedora 33 enables `systemd-resolved` by default and configures 884 | `/etc/nsswitch.conf` to use the `systemd-resolved` NSS interface; see [the 885 | Fedora changelog entry](f33-changes-systemd-resolved) for details. 886 | 887 | ## How to help 888 | 889 | If you can help with any of these areas, or have bug fixes, please fork and 890 | raise a Pull Request for me. 891 | 892 | I have built a basic test framework around the script which can be used to 893 | monitor and validate the calls made by the script based on the environment 894 | variables available to it at run-time. Please add a test for any new features 895 | you may wish to add, or update any which are wrong, and test your code by 896 | running `./run-tests` from the root of the repository. There are no dependencies 897 | on `run-tests` - it runs 100% bash and doesn't call out to any other program or 898 | language. 899 | 900 | GitHub Actions are enabled on this repository: Click the link at the top of this 901 | README to see the current state of the code and its tests. 902 | 903 | ## Development notes 904 | 905 | Please see [`HACKING.md`](./HACKING.md) for notes on developing 906 | `update-systemd-resolved`. 907 | 908 | ## Licence 909 | 910 | GPL 911 | 912 | ## Author 913 | 914 | Jonathan Wright 915 | -------------------------------------------------------------------------------- /update-systemd-resolved: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # OpenVPN helper to add DHCP information into systemd-resolved via DBus. 4 | # Copyright (C) 2016, Jonathan Wright 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # This script will parse DHCP options set via OpenVPN (dhcp-option) to update 20 | # systemd-resolved directly via DBus, instead of updating /etc/resolv.conf. To 21 | # install, set as the 'up' and 'down' script in your OpenVPN configuration file 22 | # or via the command-line arguments, alongside setting the 'down-pre' option to 23 | # run the 'down' script before the device is closed. For example: 24 | # 25 | # script-security 2 26 | # up /usr/local/libexec/openvpn/update-systemd-resolved 27 | # up-restart 28 | # down /usr/local/libexec/openvpn/update-systemd-resolved 29 | # down-pre 30 | 31 | # Define what needs to be called via DBus 32 | DBUS_DEST="org.freedesktop.resolve1" 33 | DBUS_NODE="/org/freedesktop/resolve1" 34 | 35 | SCRIPT_NAME="${BASH_SOURCE[0]##*/}" 36 | 37 | if [[ -S /dev/log ]] && command -v logger &> /dev/null; then 38 | if [[ -t 2 ]]; then 39 | log() { 40 | logger -s -t "$SCRIPT_NAME" "$@" 41 | } 42 | else 43 | # Suppress output on stderr when not attached to a (p|t)ty. 44 | # https://github.com/jonathanio/update-systemd-resolved/issues/81 45 | log() { 46 | logger -t "$SCRIPT_NAME" "$@" 47 | } 48 | fi 49 | 50 | for level in err warning info debug; do 51 | printf -v functext -- '%s() { log -p user.%s -- "$@" ; }' "$level" "$level" 52 | eval "$functext" 53 | done 54 | else 55 | log() { 56 | printf 1>&2 -- '%s: %s\n' "$SCRIPT_NAME" "$*" 57 | } 58 | 59 | for level in err warning info debug; do 60 | printf -v functext -- '%s() { log "%s:" "$@" ; }' "$level" "${level^^}" 61 | eval "$functext" 62 | done 63 | fi 64 | 65 | usage() { 66 | err "${1:?${1}. }. Usage: ${SCRIPT_NAME} up|down|print-polkit-rules []." 67 | } 68 | 69 | busctl_status() { 70 | busctl status "$DBUS_DEST" 71 | } 72 | 73 | busctl_call() { 74 | # Preserve busctl's exit status 75 | busctl call "$DBUS_DEST" "$DBUS_NODE" "${DBUS_DEST}.Manager" "$@" || { 76 | local -i status=$? 77 | err "'busctl' exited with status $status" 78 | print_polkit_rules_command_for_current_user | err 79 | return $status 80 | } 81 | } 82 | 83 | get_link_info() { 84 | dev="$1" 85 | shift 86 | 87 | link='' 88 | link="$(ip link show dev "$dev")" || return $? 89 | 90 | echo "$dev" "${link%%:*}" 91 | } 92 | 93 | each_dhcp_setting() { 94 | local foreign_option foreign_option_value setting_type setting_value 95 | 96 | for foreign_option in "${!foreign_option_@}"; do 97 | foreign_option_value="${!foreign_option}" 98 | 99 | # Matches: 100 | # 101 | # dhcp-option SOME-SETTING a-value 102 | # dhcp-option ANOTHER-SETTING 103 | # 104 | # In the second case, the setting value is the empty string. 105 | if [[ $foreign_option_value =~ ^[[:space:]]*dhcp-option[[:space:]]+([^[:space:]]+)([[:space:]]+(.*))?$ ]]; then 106 | "$@" "${BASH_REMATCH[1]}" "${BASH_REMATCH[3]-}" || return 107 | fi 108 | done 109 | } 110 | 111 | # Check that a function was supplied the expected number of arguments. If not, 112 | # issue a diagnostic message and return nonzero status. 113 | usage_for() { 114 | if [[ ${FUNCNAME[1]} != "${FUNCNAME[0]}" ]]; then 115 | usage_for 4 - "$#" ' [ ...]' || return 116 | fi 117 | 118 | local caller="${FUNCNAME[1]}" 119 | 120 | local -i argc_min 121 | 122 | case "$1" in 123 | -) 124 | argc_min=0 125 | ;; 126 | *) 127 | argc_min="$1" 128 | ;; 129 | esac 130 | 131 | shift 132 | 133 | local have_argc_max 134 | local -i argc_max 135 | 136 | case "$1" in 137 | -) ;; 138 | *) 139 | have_argc_max=yes 140 | argc_max="$1" 141 | ;; 142 | esac 143 | 144 | shift 145 | 146 | local -i argc="$1" 147 | shift 148 | 149 | if ((argc < argc_min)) || { [[ -n ${have_argc_max-} ]] && ((argc > argc_max)); }; then 150 | local expectation 151 | if [[ -n ${have_argc_max-} ]]; then 152 | if ((argc_min == argc_max)); then 153 | expectation="exactly ${argc_min}" 154 | else 155 | expectation="from ${argc_min} to ${argc_max}" 156 | fi 157 | else 158 | expectation="at least ${argc_min}" 159 | fi 160 | 161 | err "${caller}: got ${argc} argument(s); expected ${expectation}" 162 | err "usage: ${caller} $*" 163 | 164 | return 64 # EX_USAGE 165 | fi 166 | } 167 | 168 | # mapfile wrapper that (unlike "mapfile -t somevar < <(some command)") bubbles 169 | # up the exit status of the command used to generate the output read into the 170 | # mapfile'd variable. 171 | mapfile_from_command() { 172 | usage_for 2 - "$#" ' [ ...]' || return 173 | 174 | local -a passthru 175 | 176 | while (("$#" > 0)); do 177 | case "$1" in 178 | -d | -n | -O | -s | -u | -C | -c) 179 | passthru+=("$1" "$2") 180 | shift 181 | ;; 182 | -t) 183 | passthru+=("$1") 184 | ;; 185 | --) 186 | shift 187 | break 188 | ;; 189 | *) 190 | break 191 | ;; 192 | esac 193 | 194 | shift 195 | done 196 | 197 | var="$1" 198 | shift 199 | 200 | local out 201 | out="$("$@")" || return 202 | 203 | # "printf" rather than herestring ("<<<"); avoids introducing a newline 204 | mapfile "${passthru[@]}" "$var" < <(printf -- '%s' "$out") 205 | } 206 | 207 | # Work around this: 208 | # 209 | # $ IFS=$'.' read -r -a octets <<<192.168.1.1. # note trailing "." 210 | # $ echo "${#octets[@]}" 211 | # 4 212 | # $ echo "${octets[-1]}" 213 | # 1 214 | # $ mapfile -d $'.' -t octets < <(printf -- '192.168.2.1.') 215 | # $ echo "${#octets[@]}" 216 | # 4 217 | # $ echo "${octets[-1]}" 218 | # 1 219 | # 220 | # This function is like "read -r -a" or "mapfile -t", except that it adds an 221 | # empty string in the final spot in the generated array if the source string 222 | # ends with the separator sequence. 223 | # 224 | # NOTE: uses "declare -n", so requires Bash >= 4.3 225 | split_on_separator_into() { 226 | usage_for 3 3 "$#" ' ' || return 227 | 228 | local sep="$1" 229 | shift 230 | 231 | local -n rvar="$1" 232 | shift 233 | 234 | # "printf" rather than herestring ("<<<"); avoids introducing a newline. 235 | # Cannot count on "mapfile -d", which was released in the relatively-recent 236 | # Bash 5.0, so use a workaround that handles only a single line of input. 237 | IFS="$sep" read -r -a rvar < <(printf -- '%s' "$1") || : 238 | 239 | if [[ $1 == *"$sep" ]]; then 240 | rvar+=('') 241 | fi 242 | } 243 | 244 | # Print the supplied arguments as a string joined with the specified separator 245 | print_with_separator() { 246 | usage_for 1 - "$#" ' [ ...]' || return 247 | 248 | local sep="$1" 249 | shift 250 | 251 | printf -- '%s' "$1" 252 | shift 253 | 254 | if (("$#" < 1)); then 255 | return 256 | fi 257 | 258 | printf -- "${sep}%s" "$@" 259 | } 260 | 261 | # Like "print_with_separator", but adds a final newline 262 | puts_with_separator() { 263 | usage_for 1 - "$#" ' [ ...]' || return 264 | print_with_separator "$@" || return 265 | printf -- '\n' 266 | } 267 | 268 | with_openvpn_script_handling() { 269 | if (("$#" == 0)); then 270 | usage 'No script type specified' 271 | return 1 272 | fi 273 | 274 | local func="$1" 275 | shift || : 276 | 277 | local dev="${1:-${dev-}}" 278 | shift || : 279 | 280 | if [[ -z ${dev-} ]]; then 281 | usage 'No device name specified' 282 | return 1 283 | fi 284 | 285 | if ! read -r link if_index _ < <(get_link_info "$dev"); then 286 | usage "Invalid device name: '$dev'" 287 | return 1 288 | fi 289 | 290 | busctl_status &> /dev/null || { 291 | local -i status="$?" 292 | err << ERR 293 | systemd-resolved DBus interface (${DBUS_DEST}) is not available. 294 | $SCRIPT_NAME requires systemd version 229 or above. 295 | ERR 296 | return "$status" 297 | } 298 | 299 | if ! "$func" "$link" "$if_index" "$@"; then 300 | err 'Unable to configure systemd-resolved.' 301 | return 1 302 | fi 303 | } 304 | 305 | _up() { 306 | local link="$1" 307 | shift 308 | local if_index="$1" 309 | shift 310 | 311 | info "Link '$link' coming up" 312 | 313 | # Preset values for processing -- will be altered in the various process_* 314 | # functions. 315 | local -a dns_servers=() dns_ex_servers=() dns_domain=() dns_search=() dns_routed=() dnssec_negative_trust_anchors=() 316 | local -i dns_server_count=0 dns_ex_server_count=0 317 | local flush_caches=yes 318 | local dns_sec reset_statistics reset_server_features default_route 319 | local llmnr multicast_dns dns_over_tls 320 | 321 | # This function is called indirectly below (via `each_dhcp_setting`); disable 322 | # check for unreachable commands. 323 | # shellcheck disable=SC2317 324 | _dispatch_dhcp_setting() { 325 | local setting_type="${1?}" 326 | local setting_value="${2?}" 327 | 328 | process_setting_function="${setting_type,,}" 329 | process_setting_function="process_${process_setting_function//-/_}" 330 | 331 | if declare -f "$process_setting_function" &> /dev/null; then 332 | "$process_setting_function" "$setting_value" || return $? 333 | else 334 | warning "Not a recognized DHCP setting: '${setting_type}'" 335 | fi 336 | } 337 | 338 | each_dhcp_setting _dispatch_dhcp_setting || return 339 | 340 | if [[ ${reset_statistics-} == yes ]]; then 341 | info "ResetStatistics()" 342 | busctl_call ResetStatistics || return $? 343 | fi 344 | 345 | if [[ ${reset_server_features-} == yes ]]; then 346 | info 'ResetServerFeatures()' 347 | busctl_call ResetServerFeatures || return $? 348 | fi 349 | 350 | if [[ -n ${dns_sec+x} ]]; then 351 | info "SetLinkDNSSEC(${if_index} '${dns_sec}')" 352 | busctl_call SetLinkDNSSEC 'is' "$if_index" "${dns_sec}" || return 353 | fi 354 | 355 | if [[ ${#dns_servers[*]} -gt 0 ]]; then 356 | busctl_params=("$if_index" "$dns_server_count" "${dns_servers[@]}") 357 | info "SetLinkDNS(${busctl_params[*]})" 358 | busctl_call SetLinkDNS 'ia(iay)' "${busctl_params[@]}" || return $? 359 | fi 360 | 361 | if [[ ${#dns_ex_servers[*]} -gt 0 ]]; then 362 | busctl_params=("$if_index" "$dns_ex_server_count" "${dns_ex_servers[@]}") 363 | info "SetLinkDNSEx(${busctl_params[*]})" 364 | busctl_call SetLinkDNSEx 'ia(iayqs)' "${busctl_params[@]}" || return $? 365 | fi 366 | 367 | # Divide by two to account for the boolean second argument 368 | dns_count="$(((${#dns_domain[*]} + ${#dns_search[*]} + ${#dns_routed[*]}) / 2))" 369 | if ((dns_count > 0)); then 370 | busctl_params=( 371 | "$if_index" 372 | "$dns_count" 373 | 374 | # Hack to work around pre-4.4 Bash `empty array == unset` bug 375 | ${dns_domain:+"${dns_domain[@]}"} 376 | ${dns_search:+"${dns_search[@]}"} 377 | ${dns_routed:+"${dns_routed[@]}"} 378 | ) 379 | info "SetLinkDomains(${busctl_params[*]})" 380 | busctl_call SetLinkDomains 'ia(sb)' "${busctl_params[@]}" || return $? 381 | fi 382 | 383 | if [[ -n ${default_route-} ]]; then 384 | info "SetLinkDefaultRoute(${if_index} ${default_route})" 385 | busctl_call SetLinkDefaultRoute 'ib' "$if_index" "$default_route" || return $? 386 | fi 387 | 388 | if [[ -n ${llmnr+x} ]]; then 389 | info "SetLinkLLMNR(${if_index} '${llmnr}')" 390 | busctl_call SetLinkLLMNR 'is' "$if_index" "$llmnr" 391 | fi 392 | 393 | if [[ -n ${multicast_dns+x} ]]; then 394 | info "SetLinkMulticastDNS(${if_index} '${multicast_dns}')" 395 | busctl_call SetLinkMulticastDNS 'is' "$if_index" "$multicast_dns" 396 | fi 397 | 398 | if [[ -n ${dns_over_tls+x} ]]; then 399 | info "SetLinkDNSOverTLS(${if_index} '${dns_over_tls}')" 400 | busctl_call SetLinkDNSOverTLS 'is' "$if_index" "$dns_over_tls" 401 | fi 402 | 403 | if (("${#dnssec_negative_trust_anchors[*]}" > 0)); then 404 | busctl_params=( 405 | "$if_index" 406 | "${#dnssec_negative_trust_anchors[*]}" 407 | "${dnssec_negative_trust_anchors[@]}" 408 | ) 409 | 410 | info "SetLinkDNSSECNegativeTrustAnchors(${busctl_params[*]})" 411 | busctl_call SetLinkDNSSECNegativeTrustAnchors ias "${busctl_params[@]}" 412 | fi 413 | 414 | if [[ -n ${flush_caches-} ]]; then 415 | info 'FlushCaches()' 416 | busctl_call FlushCaches || return 417 | fi 418 | } 419 | 420 | up() { 421 | with_openvpn_script_handling _up "$@" 422 | } 423 | 424 | down() { 425 | with_openvpn_script_handling _down "$@" 426 | } 427 | 428 | _down() { 429 | local link="$1" 430 | shift 431 | local if_index="$1" 432 | shift 433 | 434 | info "Link '$link' going down" 435 | 436 | if ! busctl_call RevertLink i "$if_index"; then 437 | info 'Calling RevertLink failed; this can happen if privileges were dropped in the OpenVPN client.' 438 | print_polkit_rules_command_for_current_user | info 439 | fi 440 | } 441 | 442 | # Run sipcalc and extract a single line matching the provided prefix 443 | match_sipcalc_output() { 444 | usage_for 2 2 "$#" '
' || return 445 | 446 | local prefix="$1" 447 | shift 448 | 449 | local out 450 | out="$(sipcalc "$@" 2> >(err))" || return 451 | 452 | while read -r line; do 453 | if [[ $line == "$prefix"* ]]; then 454 | printf -- '%s\n' "${line##*- }" 455 | return 456 | fi 457 | done <<< "$out" 458 | 459 | return 1 460 | } 461 | 462 | # Expand an IPv4 or IPv6 address using Python's "ipaddress" module 463 | expand_ip_python() { 464 | usage_for 2 2 "$#" '{IPv4,IPv6}
' || return 465 | 466 | local type="$1" 467 | shift 468 | 469 | case "$type" in 470 | IPv4 | IPv6) ;; 471 | *) 472 | err "${FUNCNAME[0]}: not a valid IP version type: ${type}" 473 | return 64 474 | ;; 475 | esac 476 | 477 | python -c " 478 | import ipaddress 479 | import sys 480 | 481 | # Abort if we're on an older Python; the backported 'ipaddress' module requires 482 | # IPs to be unicode, and properly decoding sys.argv is problematic on Python 2 483 | # (see https://bugs.python.org/issue2128). 484 | if sys.version_info < (3, 0): 485 | majmin = '.'.join([str(v) for v in sys.version_info[0:2]]) 486 | sys.stderr.write('${type} address expansion is not supported for Python {0}\\n'.format(majmin)) 487 | sys.exit(1) 488 | 489 | try: 490 | print(ipaddress.${type}Address(sys.argv[1]).exploded) 491 | except Exception as e: 492 | sys.stderr.write(\"'{0}' is not a valid ${type} address: {1}\\n\".format(sys.argv[1], e)) 493 | sys.exit(1) 494 | " "${1?}" 2> >(err) 495 | } 496 | 497 | # Very light check to see if a string looks vaguely in the vicinity of an IPv4 498 | # address; more robust validation occurs in (the course of executing) 499 | # "parse_ipv4". 500 | # 501 | # NOTE that we include the "! looks_like_ipv6" condition in order return a 502 | # nonzero status when provided an IPv4-in-IPv6 address (e.g. "::ffff:1.2.3.4"). 503 | # This check comes after the check for dotted-quad so that we can do 504 | # 505 | # if looks_like_ipv6 "$address"; then 506 | # process_dns_ipv6 "$address" || return $? 507 | # elif looks_like_ipv4 "$address"; then 508 | # process_dns_ipv4 "$address" || return $? 509 | # else 510 | # 511 | # without repeating work. 512 | looks_like_ipv4() { 513 | [[ ${1-} =~ ^([^.]+\.){3}[^.]+$ ]] && ! looks_like_ipv6 "${1-}" 514 | } 515 | 516 | # Read the components of a dotted-quad IPv4 into the specified array variable 517 | read_ipv4_segments_into() { 518 | usage_for 2 2 "$#" ' ' || return 519 | 520 | split_on_separator_into $'.' "$@" 521 | } 522 | 523 | each_ipv4_segment() { 524 | looks_like_ipv4 "$@" || return 525 | 526 | local -a segments 527 | read_ipv4_segments_into segments "$@" || return 528 | 529 | ((${#segments[@]} == 4)) || return 530 | 531 | local segment 532 | for segment in "${segments[@]}"; do 533 | printf -- '%s\n' "$segment" 534 | done 535 | } 536 | 537 | expand_ipv4_native() { 538 | local address="$1" 539 | 540 | local -a segments 541 | mapfile_from_command -t segments each_ipv4_segment "$address" || return 542 | 543 | log_invalid_ipv4() { 544 | local message="'$address' is not a valid IPv4 address" 545 | err "${message}: $*" 546 | unset -f "${FUNCNAME[0]:-log_invalid_ipv4}" 547 | return 1 548 | } 549 | 550 | local segment 551 | local -i decimal_segment 552 | 553 | for segment in "${segments[@]}"; do 554 | printf -v decimal_segment -- '%d' "$segment" 2> /dev/null || { 555 | local -i status="$?" 556 | log_invalid_ipv4 "cannot interpret '${segment}' as a decimal number" 557 | return "$status" 558 | } 559 | 560 | if ((decimal_segment < 0)) || ((decimal_segment > 255)); then 561 | log_invalid_ipv4 "'${segment}' is not a decimal number from 0 to 255, inclusive" 562 | return 1 563 | fi 564 | done 565 | 566 | puts_with_separator $'.' "${segments[@]}" 567 | } 568 | 569 | expand_ipv4_sipcalc() { 570 | match_sipcalc_output 'Host address' "$@" 571 | } 572 | 573 | expand_ipv4_python() { 574 | expand_ip_python IPv4 "$@" 575 | } 576 | 577 | parse_ipv4() { 578 | local expanded 579 | expanded="$(expand_ipv4 "$@")" || return 580 | each_ipv4_segment "$expanded" 581 | } 582 | 583 | # Very light check to see if a string looks vaguely in the vicinity of an IPv6 584 | # address; more robust validation occurs in (the course of executing) 585 | # "parse_ipv6". 586 | looks_like_ipv6() { 587 | [[ ${1-} == *:*:* ]] 588 | } 589 | 590 | read_ipv6_segments_into() { 591 | usage_for 2 2 "$#" '
' || return 592 | 593 | split_on_separator_into $':' "$@" 594 | } 595 | 596 | each_ipv6_segment() { 597 | looks_like_ipv6 "$@" || return 598 | 599 | local -a segments 600 | read_ipv6_segments_into segments "$@" || return 601 | 602 | ((${#segments[@]} == 8)) || return 603 | 604 | local segment 605 | for segment in "${segments[@]}"; do 606 | printf -- '%s\n' "$segment" 607 | done 608 | } 609 | 610 | expand_ipv6_native() { 611 | local orig_address="${1-}" 612 | 613 | log_invalid_ipv6() { 614 | local message="'$orig_address' is not a valid IPv6 address" 615 | err "${message}: $*" 616 | unset -f "${FUNCNAME[0]:-log_invalid_ipv6}" 617 | return 1 618 | } 619 | 620 | local -a orig_segments 621 | read_ipv6_segments_into orig_segments "$orig_address" || { 622 | local -i status="$?" 623 | log_invalid_ipv6 'failed to read address segments' 624 | return "$status" 625 | } 626 | 627 | if (("${#orig_segments[@]}" < 3)); then 628 | log_invalid_ipv6 "expected at least 3 address segments; got ${#orig_segments[@]}" 629 | return 1 630 | fi 631 | 632 | if looks_like_ipv4 "${orig_segments[-1]-}"; then 633 | local -a ipv4_segments 634 | 635 | mapfile_from_command -t ipv4_segments parse_ipv4 "${orig_segments[-1]}" || { 636 | local -i status="$?" 637 | log_invalid_ipv6 "failed to parse embedded IPv4 address '${orig_segments[-1]}'" 638 | return "$status" 639 | } 640 | 641 | printf -v 'orig_segments[-1]' -- '%0.2x%0.2x' "${ipv4_segments[@]:0:2}" 642 | printf -v "orig_segments[${#orig_segments[@]}]" -- '%0.2x%0.2x' "${ipv4_segments[@]:2:4}" 643 | fi 644 | 645 | local -i expected_len=8 646 | local -i orig_len="${#orig_segments[@]}" 647 | 648 | # "expected_len + 1" to account for addresses like "::1:1:1:1:1:1:1" 649 | if ((orig_len > (expected_len + 1))); then 650 | log_invalid_ipv6 "at most ${expected_len} colons permitted; got $((orig_len - 1))" 651 | return 1 652 | fi 653 | 654 | local -a final_segments 655 | local final_segment 656 | local -i orig_idx 657 | local saw_compressed_group 658 | local -i zero_segments_needed_count 659 | 660 | for ((orig_idx = 0; orig_idx < orig_len; orig_idx++)); do 661 | orig_segment="${orig_segments[orig_idx]}" 662 | 663 | if [[ -z $orig_segment ]]; then 664 | if [[ -n ${saw_compressed_group-} ]]; then 665 | log_invalid_ipv6 "at most one '::' permitted" 666 | return 1 667 | fi 668 | 669 | saw_compressed_group=yes 670 | 671 | if ((orig_idx == 0)); then 672 | # ::1:2:3:4 673 | if [[ -z ${orig_segments[$((orig_idx + 1))]-} ]]; then 674 | zero_segments_needed_count="$(((expected_len - orig_len) + 2))" 675 | ((orig_idx++)) 676 | # :1:2:3:4 677 | else 678 | log_invalid_ipv6 "leading ':' without '::'" 679 | return 1 680 | fi 681 | # 1:2:3:4:: 682 | elif { 683 | ((orig_idx == (orig_len - 2))) && 684 | [[ -z ${orig_segments[$((orig_idx + 1))]-} ]] 685 | }; then 686 | zero_segments_needed_count="$(((expected_len - orig_len) + 2))" 687 | ((orig_idx++)) 688 | # 1:2:3:4: 689 | elif ((orig_idx == (orig_len - 1))); then 690 | log_invalid_ipv6 "trailing ':' without '::'" 691 | return 1 692 | # 1:2::3:4 693 | else 694 | zero_segments_needed_count="$(((expected_len - orig_len) + 1))" 695 | fi 696 | 697 | if ((zero_segments_needed_count < 1)); then 698 | log_invalid_ipv6 "cannot expand '::'; address already has 8 or more segments" 699 | return 1 700 | fi 701 | 702 | local -i zero_segment_counter 703 | for ((\ 704 | zero_segment_counter = 0; \ 705 | zero_segment_counter < zero_segments_needed_count; \ 706 | zero_segment_counter++)); do 707 | final_segments+=(0000) 708 | done 709 | elif (("${#orig_segment}" > 4)); then 710 | log_invalid_ipv6 "'$orig_segment' is longer than 4 characters" 711 | return 1 712 | else 713 | printf -v final_segment -- '%0.4x' "0x${orig_segment}" 2> /dev/null || { 714 | local -i status="$?" 715 | log_invalid_ipv6 "cannot interpret '${orig_segment}' as a hexadecimal number" 716 | return "$status" 717 | } 718 | 719 | final_segments+=("$final_segment") 720 | fi 721 | done 722 | 723 | if (("${#final_segments[@]}" != expected_len)); then 724 | log_invalid_ipv6 "expected ${expected_len} segments; got ${#final_segments[@]}" 725 | return 1 726 | fi 727 | 728 | puts_with_separator $':' "${final_segments[@]}" 729 | } 730 | 731 | expand_ipv6_sipcalc() { 732 | match_sipcalc_output 'Expanded Address' "$@" 733 | } 734 | 735 | expand_ipv6_python() { 736 | expand_ip_python IPv6 "$@" 737 | } 738 | 739 | test_ipv4_expansion_func() { 740 | local expanded 741 | expanded="$("${1?}" 127.0.0.1)" && [[ $expanded == '127.0.0.1' ]] 742 | } 743 | 744 | test_ipv6_expansion_func() { 745 | local expanded 746 | expanded="$("${1?}" ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff)" && 747 | [[ $expanded == ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff ]] 748 | } 749 | 750 | each_ip_expansion_func() { 751 | local cb="$1" 752 | shift 753 | 754 | local type name 755 | 756 | case "${1,,}" in 757 | ipv4) 758 | type="${1,,}" 759 | name=IPv4 760 | ;; 761 | ipv6) 762 | type="${1,,}" 763 | name=IPv6 764 | ;; 765 | *) 766 | err "unrecognized IP version type '${1}'" 767 | return 64 768 | ;; 769 | esac 770 | 771 | local expansion_func="expand_${type}" 772 | local expansion_func_impl 773 | local impl 774 | 775 | for impl in python sipcalc native; do 776 | expansion_func_impl="${expansion_func}_${impl}" 777 | 778 | # Run in subshell with `logger` defined as a NOP to avoid issuing useless 779 | # messages about (say) not being able to find the `python` or `sipcalc` 780 | # programs. 781 | # `log` is called indirectly; disable warning about unreachable command. 782 | # shellcheck disable=SC2317 783 | if ( 784 | log() { :; } 785 | "test_${type}_expansion_func" "$expansion_func_impl" 786 | ); then 787 | if "$cb" "$expansion_func_impl" 1; then 788 | return 789 | fi 790 | elif "$cb" "$expansion_func_impl" 0; then 791 | return 792 | fi 793 | done 794 | } 795 | 796 | set_up_ip_expansion_func() { 797 | local type name 798 | 799 | case "${1,,}" in 800 | ipv4) 801 | type="${1,,}" 802 | name=IPv4 803 | ;; 804 | ipv6) 805 | type="${1,,}" 806 | name=IPv6 807 | ;; 808 | *) 809 | err "unrecognized IP version type '${1}'" 810 | return 64 811 | ;; 812 | esac 813 | 814 | local preference_var="UPDATE_SYSTEMD_RESOLVED_PREFERRED_${type^^}_EXPANSION_IMPLEMENTATION" 815 | 816 | local preference 817 | if [[ -v $preference_var ]]; then 818 | preference="${!preference_var}" 819 | fi 820 | 821 | preference="${preference:-${UPDATE_SYSTEMD_RESOLVED_PREFERRED_IP_EXPANSION_IMPLEMENTATION-}}" 822 | 823 | local expansion_func="expand_${type}" 824 | 825 | local expansion_func_impl 826 | 827 | if [[ -n $preference ]]; then 828 | expansion_func_impl="${expansion_func}_${preference}" 829 | 830 | if declare -f "$expansion_func_impl" &> /dev/null; then 831 | eval "${expansion_func}() { $expansion_func_impl \"\$@\"; }" 832 | else 833 | err "${preference} is not a valid ${name} address expansion implementation" 834 | exit 1 835 | fi 836 | fi 837 | 838 | if ! declare -f "$expansion_func" &> /dev/null; then 839 | # This function is called indirectly below (via `each_ip_expansion_func`); 840 | # disable check for unreachable commands. 841 | # shellcheck disable=SC2317 842 | choose_expansion_func_impl() { 843 | expansion_func_impl="$1" 844 | 845 | if (("$2" == 1)); then 846 | eval "${expansion_func}() { $expansion_func_impl \"\$@\"; }" 847 | else 848 | return 1 849 | fi 850 | } 851 | 852 | each_ip_expansion_func choose_expansion_func_impl "$type" 853 | 854 | unset -f choose_expansion_func_impl 855 | fi 856 | 857 | if ! declare -f "$expansion_func" &> /dev/null; then 858 | err "no usable ${name} expansion implementations" 859 | return 1 860 | fi 861 | } 862 | 863 | # "builtin exit" because the test suite overrides "exit". If we cannot handle 864 | # IP addresses, no sense in continuing. 865 | set_up_ip_expansion_func ipv4 || builtin exit 866 | set_up_ip_expansion_func ipv6 || builtin exit 867 | 868 | parse_ipv6() { 869 | local expanded 870 | expanded="$(expand_ipv6 "$@")" || return 871 | each_ipv6_segment "$expanded" 872 | } 873 | 874 | parse_dns_spec() { 875 | usage_for 1 - "$#" ' [ ]' || return 876 | 877 | local spec="$1" 878 | shift 879 | 880 | local -n address_ref="${1:-address}" 881 | shift 882 | 883 | local -n port_ref="${1:-port}" 884 | shift 885 | 886 | local -n server_name_ref="${1:-server_name}" 887 | shift 888 | 889 | local cursor="$spec" 890 | while [[ -n ${cursor-} ]]; do 891 | case "$cursor" in 892 | *'#'?*) 893 | server_name_ref="${cursor#*'#'}" 894 | cursor="${cursor%%'#'*}" 895 | ;; 896 | *:?*) 897 | if looks_like_ipv6 "$cursor" &> /dev/null; then 898 | address_ref="$cursor" 899 | break 900 | else 901 | case "$cursor" in 902 | '['*']'*) 903 | case "$cursor" in 904 | '['*']:'?*) 905 | address_ref="${cursor#[}" 906 | address_ref="${address_ref#]}" 907 | port_ref="${cursor#*:}" 908 | break 909 | ;; 910 | *) 911 | err "invalid DNS server specification '${spec}'" 912 | return 1 913 | ;; 914 | esac 915 | ;; 916 | *) 917 | address_ref="${cursor%%:*}" 918 | port_ref="${cursor#*:}" 919 | break 920 | ;; 921 | esac 922 | fi 923 | ;; 924 | *) 925 | address_ref="$cursor" 926 | break 927 | ;; 928 | esac 929 | done 930 | 931 | # Ensure that port variable is defined if server name variable is. The 932 | # default value is `0`, meaning "default port 53" when passed to 933 | # `SetLinkDNSEx`. 934 | # 935 | # NOTE that we do not do any further input validation here; instead we let 936 | # `SetLinkDNSEx` complain if the port is anything other than an unsigned 937 | # integer < 2 ** 16. 938 | if [[ -n ${server_name_ref-} ]]; then 939 | port_ref="${port_ref:-0}" 940 | fi 941 | 942 | # Ensure that server name variable is defined if port variable is. The 943 | # default value is the empty string, meaning "no server name" when passed to 944 | # `SetLinkDNSEx`. 945 | if [[ -n ${port_ref-} ]]; then 946 | server_name_ref="${server_name_ref-}" 947 | fi 948 | } 949 | 950 | process_dns() { 951 | local spec="$1" 952 | shift 953 | 954 | local address port server_name 955 | parse_dns_spec "$spec" address port server_name || return 956 | 957 | local -a args=() 958 | if [[ -n ${port-} ]] || [[ -n ${server_name-} ]]; then 959 | args=("${port:-0}" "${server_name-}") 960 | fi 961 | 962 | if looks_like_ipv6 "$address"; then 963 | process_dns_ipv6 "$address" "${args[@]}" || return 964 | elif looks_like_ipv4 "$address"; then 965 | process_dns_ipv4 "$address" "${args[@]}" || return 966 | else 967 | err "Not a valid IPv6 or IPv4 address: '${address}' (full specification: '${spec}')" 968 | return 1 969 | fi 970 | } 971 | 972 | process_dns6() { 973 | process_dns "$@" 974 | } 975 | 976 | process_dns_ipv4() { 977 | usage_for 1 3 "$#" '
[ ]' || return 978 | 979 | local address="$1" 980 | shift 981 | 982 | info "Adding IPv4 DNS Server ${address}" 983 | 984 | local -a segments 985 | mapfile_from_command -t segments parse_ipv4 "$address" || return 986 | 987 | if (("$#" > 0)); then 988 | dns_ex_servers+=(2 4 "${segments[@]}" "${1:-0}" "${2-}") 989 | ((dns_ex_server_count += 1)) 990 | else 991 | dns_servers+=(2 4 "${segments[@]}") 992 | ((dns_server_count += 1)) 993 | fi 994 | } 995 | 996 | process_dns_ipv6() { 997 | usage_for 1 3 "$#" '
[ ]' || return 998 | 999 | local address="$1" 1000 | shift 1001 | 1002 | info "Adding IPv6 DNS Server ${address}" 1003 | 1004 | local -a segments 1005 | mapfile_from_command -t segments parse_ipv6 "$address" || return 1006 | 1007 | if (("$#" > 0)); then 1008 | # Add AF_INET6 and byte count 1009 | dns_ex_servers+=(10 16) 1010 | for segment in "${segments[@]}"; do 1011 | dns_ex_servers+=("$((16#${segment:0:2}))" "$((16#${segment:2:2}))") 1012 | done 1013 | 1014 | dns_ex_servers+=("${1:-0}" "${2-}") 1015 | 1016 | ((dns_ex_server_count += 1)) 1017 | else 1018 | # Add AF_INET6 and byte count 1019 | dns_servers+=(10 16) 1020 | for segment in "${segments[@]}"; do 1021 | dns_servers+=("$((16#${segment:0:2}))" "$((16#${segment:2:2}))") 1022 | done 1023 | 1024 | ((dns_server_count += 1)) 1025 | fi 1026 | } 1027 | 1028 | process_domain() { 1029 | local domain="$1" 1030 | shift 1031 | 1032 | info "Adding DNS Domain ${domain}" 1033 | 1034 | # Make sure the first domain specified with "dhcp-option DOMAIN " 1035 | # appears at the head of the list we pass to SetLinkDNS. 1036 | if (("${#dns_domain[*]}" == 0)); then 1037 | dns_domain+=("${domain}" false) 1038 | else 1039 | dns_search+=("${domain}" false) 1040 | fi 1041 | } 1042 | 1043 | process_adapter_domain_suffix() { 1044 | # This enables support for ADAPTER_DOMAIN_SUFFIX which is a Microsoft standard 1045 | # which works in the same way as DOMAIN to set the primary search domain on 1046 | # this specific link. 1047 | process_domain "$@" 1048 | } 1049 | 1050 | process_domain_search() { 1051 | local domain="$1" 1052 | shift 1053 | 1054 | info "Adding DNS Search Domain ${domain}" 1055 | dns_search+=("${domain}" false) 1056 | } 1057 | 1058 | process_domain_route() { 1059 | local domain="$1" 1060 | shift 1061 | 1062 | info "Adding DNS Routed Domain ${domain}" 1063 | dns_routed+=("${domain}" true) 1064 | } 1065 | 1066 | process_dnssec() { 1067 | case "${1,,}" in 1068 | yes | true) 1069 | dns_sec=yes 1070 | ;; 1071 | no | false) 1072 | dns_sec=no 1073 | ;; 1074 | allow-downgrade) 1075 | dns_sec=allow-downgrade 1076 | ;; 1077 | default) 1078 | dns_sec="" 1079 | ;; 1080 | *) 1081 | err "'$1' is not a valid DNSSEC option" 1082 | return 1 1083 | ;; 1084 | esac 1085 | 1086 | info "Setting DNSSEC to ${dns_sec:-default}" 1087 | } 1088 | 1089 | process_reset_statistics() { 1090 | case "${1,,}" in 1091 | yes | true) 1092 | reset_statistics=yes 1093 | ;; 1094 | no | false) 1095 | reset_statistics="" 1096 | ;; 1097 | *) 1098 | err "'$1' is not a valid value for RESET-STATISTICS" 1099 | return 1 1100 | ;; 1101 | esac 1102 | } 1103 | 1104 | process_flush_caches() { 1105 | case "${1,,}" in 1106 | yes | true) 1107 | flush_caches=yes 1108 | ;; 1109 | no | false) 1110 | flush_caches="" 1111 | ;; 1112 | *) 1113 | err "'$1' is not a valid value for FLUSH-CACHES" 1114 | return 1 1115 | ;; 1116 | esac 1117 | } 1118 | 1119 | process_reset_server_features() { 1120 | case "${1,,}" in 1121 | yes | true) 1122 | reset_server_features=yes 1123 | ;; 1124 | no | false) 1125 | reset_server_features="" 1126 | ;; 1127 | *) 1128 | err "'$1' is not a valid value for RESET-SERVER-FEATURES" 1129 | return 1 1130 | ;; 1131 | esac 1132 | } 1133 | 1134 | process_default_route() { 1135 | case "${1,,}" in 1136 | yes | true) 1137 | default_route=true 1138 | ;; 1139 | no | false) 1140 | default_route=false 1141 | ;; 1142 | *) 1143 | err "'$1' is not a valid value for DEFAULT-ROUTE" 1144 | return 1 1145 | ;; 1146 | esac 1147 | 1148 | info "Setting DEFAULT-ROUTE to ${default_route}" 1149 | } 1150 | 1151 | process_llmnr() { 1152 | case "${1,,}" in 1153 | yes | true) 1154 | llmnr=yes 1155 | ;; 1156 | no | false) 1157 | llmnr=no 1158 | ;; 1159 | resolve) 1160 | llmnr=resolve 1161 | ;; 1162 | default) 1163 | llmnr="" 1164 | ;; 1165 | *) 1166 | err "'$1' is not a valid value for LLMNR" 1167 | return 1 1168 | ;; 1169 | esac 1170 | 1171 | info "Setting LLMNR to ${llmnr:-default}" 1172 | } 1173 | 1174 | process_multicast_dns() { 1175 | case "${1,,}" in 1176 | yes | true) 1177 | multicast_dns=yes 1178 | ;; 1179 | no | false) 1180 | multicast_dns=no 1181 | ;; 1182 | resolve) 1183 | multicast_dns=resolve 1184 | ;; 1185 | default) 1186 | multicast_dns="" 1187 | ;; 1188 | *) 1189 | err "'$1' is not a valid value for MULTICAST-DNS" 1190 | return 1 1191 | ;; 1192 | esac 1193 | 1194 | info "Setting MULTICAST-DNS to ${multicast_dns:-default}" 1195 | } 1196 | 1197 | process_dns_over_tls() { 1198 | case "${1,,}" in 1199 | yes | true) 1200 | dns_over_tls=yes 1201 | ;; 1202 | no | false) 1203 | dns_over_tls=no 1204 | ;; 1205 | opportunistic) 1206 | dns_over_tls=opportunistic 1207 | ;; 1208 | default) 1209 | dns_over_tls="" 1210 | ;; 1211 | *) 1212 | err "'$1' is not a valid value for DNS-OVER-TLS" 1213 | return 1 1214 | ;; 1215 | esac 1216 | 1217 | info "Setting DNS-OVER-TLS to ${dns_over_tls:-default}" 1218 | } 1219 | 1220 | process_dnssec_negative_trust_anchors() { 1221 | local domain="$1" 1222 | shift 1223 | 1224 | info "Adding DNSSEC negative trust anchor ${domain}" 1225 | dnssec_negative_trust_anchors+=("$domain") 1226 | } 1227 | 1228 | to_json_array_jq() { 1229 | jq --compact-output --null-input '$ARGS.positional' --args -- "$@" 1230 | } 1231 | 1232 | to_json_array_perl() { 1233 | perl -MModule::Load -wle ' 1234 | foreach my $mod ( qw(Cpanel::JSON::XS JSON::MaybeXS JSON::XS JSON::PP JSON) ) { 1235 | if ( eval { load $mod; $mod->import(qw(encode_json)); 1 } ) { 1236 | print encode_json(\@ARGV); 1237 | last; 1238 | } 1239 | } 1240 | ' -- "$@" 1241 | } 1242 | 1243 | to_json_array_python() { 1244 | python -c " 1245 | import sys 1246 | 1247 | try: 1248 | import json 1249 | except ImportError: 1250 | import simplejson as json 1251 | 1252 | print(json.dumps(sys.argv[1:])) 1253 | " "$@" 1254 | } 1255 | 1256 | to_json_array_native() { 1257 | printf -- '[' 1258 | 1259 | while (("$#" > 0)); do 1260 | printf -- '"%s"' "${1//\"/\\\"}" 1261 | shift 1262 | if (("$#" > 0)); then 1263 | printf -- ',' 1264 | fi 1265 | done 1266 | 1267 | printf -- ']' 1268 | } 1269 | 1270 | test_to_json_array_func() { 1271 | local expanded 1272 | expanded="$("${1?}" foo bar baz)" && [[ $expanded =~ ^\['"foo",'[[:space:]]*'"bar",'[[:space:]]*'"baz"'\]$ ]] 1273 | } 1274 | 1275 | set_up_to_json_array_func() { 1276 | local expansion_func_impl 1277 | local impl 1278 | 1279 | for impl in jq perl python native; do 1280 | expansion_func_impl="to_json_array_${impl}" 1281 | if test_to_json_array_func "$expansion_func_impl" 2> /dev/null; then 1282 | eval "to_json_array() { $expansion_func_impl \"\$@\"; }" 1283 | return 1284 | fi 1285 | done 1286 | 1287 | return 1 1288 | } 1289 | 1290 | if ! set_up_to_json_array_func; then 1291 | to_json_array_func() { 1292 | printf -- 'Unable to serialize arguments to a JSON array' 1293 | return 127 1294 | } 1295 | fi 1296 | 1297 | require_optarg() { 1298 | local opt="$1" 1299 | shift 1300 | 1301 | local argc="$1" 1302 | shift 1303 | 1304 | if ((argc < 2)); then 1305 | err "missing required argument for option \"$opt\"" 1306 | return 1 1307 | fi 1308 | } 1309 | 1310 | # shellcheck disable=SC2120 1311 | print_polkit_rules() { 1312 | local -A allowed_users_map=() allowed_groups_map=() systemd_openvpn_units_map=() 1313 | 1314 | while (("$#" > 0)); do 1315 | case "$1" in 1316 | --polkit-allowed-user) 1317 | require_optarg "$1" "$#" || return 1318 | allowed_users_map["${2?}"]=1 1319 | shift 1320 | ;; 1321 | --polkit-allowed-user=?*) 1322 | allowed_users_map["${1#*=}"]=1 1323 | ;; 1324 | --polkit-allowed-group) 1325 | require_optarg "$1" "$#" || return 1326 | allowed_groups_map["${2?}"]=1 1327 | shift 1328 | ;; 1329 | --polkit-allowed-group=?*) 1330 | allowed_groups_map["${1#*=}"]=1 1331 | ;; 1332 | --polkit-systemd-openvpn-unit) 1333 | require_optarg "$1" "$#" || return 1334 | systemd_openvpn_units_map["${2?}"]=1 1335 | shift 1336 | ;; 1337 | --polkit-systemd-openvpn-unit=?*) 1338 | systemd_openvpn_units_map["${1#*=}"]=1 1339 | ;; 1340 | *) 1341 | err "unrecognized option: $1" 1342 | return 1 1343 | ;; 1344 | esac 1345 | 1346 | shift 1347 | done 1348 | 1349 | if { 1350 | (("${#systemd_openvpn_units_map[@]}" < 1)) && 1351 | (("${#allowed_users_map[@]}" < 1)) && 1352 | (("${#allowed_groups_map[@]}" < 1)) 1353 | }; then 1354 | # NOTE that we cannot use the template unit "openvpn-client@.service" 1355 | # itself: 1356 | # 1357 | # $ systemctl show -p User openvpn-client@.service 1358 | # Failed to get properties: Unit name openvpn-client@.service is neither a valid invocation ID nor unit name. 1359 | # $ systemctl show -p User openvpn-client@utterly-bogus.service 1360 | # User=openvpn 1361 | # 1362 | systemd_openvpn_units_map["openvpn-client@totally-made-up-to-avoid-collisions-${RANDOM:-12345}.service"]=1 1363 | fi 1364 | 1365 | local allowed_user 1366 | while read -r allowed_user; do 1367 | if [[ -n ${allowed_user-} ]]; then 1368 | allowed_users_map["$allowed_user"]=1 1369 | fi 1370 | done < <(systemctl show -P User "${!systemd_openvpn_units_map[@]}" 2> /dev/null) 1371 | 1372 | if ((${#allowed_users_map[@]} < 1)); then 1373 | warning 'unable to determine the value(s) of "User=..." for OpenVPN client systemd units; assuming "root".' 1374 | allowed_users_map[root]=1 1375 | fi 1376 | 1377 | local allowed_group 1378 | while read -r allowed_group; do 1379 | if [[ -n ${allowed_group-} ]]; then 1380 | allowed_groups_map["$allowed_group"]=1 1381 | fi 1382 | done < <(systemctl show -P Group "${!systemd_openvpn_units_map[@]}" 2> /dev/null) 1383 | 1384 | if ((${#allowed_groups_map[@]} < 1)); then 1385 | err 'unable to determine the value(s) of "Group=..." for OpenVPN client systemd units; assuming "root".' 1386 | allowed_groups_map[root]=1 1387 | fi 1388 | 1389 | local allowed_users allowed_groups 1390 | allowed_users="$(to_json_array "${!allowed_users_map[@]}")" || return 1391 | allowed_groups="$(to_json_array "${!allowed_groups_map[@]}")" || return 1392 | 1393 | printf -- \ 1394 | '/* 1395 | * Allow OpenVPN client services to update systemd-resolved settings. 1396 | * Added by %s. 1397 | */ 1398 | 1399 | function listToBoolMap(list) { 1400 | var result = {}; 1401 | 1402 | for (var i = 0; i < list.length; i++) { 1403 | var item = list[i]; 1404 | result[item] = true; 1405 | } 1406 | 1407 | return result; 1408 | } 1409 | 1410 | const updateSystemdResolved = { 1411 | allowedUsers: listToBoolMap(%s), 1412 | 1413 | allowedGroups: %s, 1414 | 1415 | allowedSubactions: listToBoolMap([ 1416 | "set-dns-servers", 1417 | "set-domains", 1418 | "set-default-route", 1419 | "set-llmnr", 1420 | "set-mdns", 1421 | "set-dns-over-tls", 1422 | "set-dnssec", 1423 | "set-dnssec-negative-trust-anchors", 1424 | "revert" 1425 | ]), 1426 | 1427 | actionIsAllowed: function(action) { 1428 | if ( !action.id.startsWith("org.freedesktop.resolve1.") ) { 1429 | return false; 1430 | } 1431 | 1432 | var ns = action.id.split("."); 1433 | var subaction = ns[ns.length - 1]; 1434 | 1435 | return this.allowedSubactions[subaction]; 1436 | }, 1437 | 1438 | subjectIsAllowed: function(subject) { 1439 | if ( this.allowedUsers[subject.user] ) { 1440 | return true; 1441 | } 1442 | 1443 | return this.allowedGroups.some(function(group) { 1444 | subject.isInGroup(group); 1445 | }); 1446 | }, 1447 | 1448 | isAllowed: function(action, subject) { 1449 | return this.actionIsAllowed(action) && this.subjectIsAllowed(subject); 1450 | } 1451 | }; 1452 | 1453 | polkit.addRule(function(action, subject) { 1454 | if ( updateSystemdResolved.isAllowed(action, subject) ) { 1455 | return polkit.Result.YES; 1456 | } else { 1457 | return polkit.Result.NOT_HANDLED; 1458 | } 1459 | }); 1460 | ' "$SCRIPT_NAME" "$allowed_users" "$allowed_groups" 1461 | } 1462 | 1463 | print_polkit_rules_command_for_current_user() { 1464 | local current_user current_group 1465 | 1466 | local format='You may wish to add the output of the following command' 1467 | format+=' to your polkit rules in order to authorize your user to access' 1468 | format+=' the systemd-resolved DBus interface:' 1469 | format+='\n%q print-polkit-rules' 1470 | 1471 | local -a args=("$SCRIPT_NAME") 1472 | 1473 | if current_user="$(id -u -n 2> /dev/null)" && [[ -n ${current_user-} ]]; then 1474 | format+=' --polkit-allowed-user %q' 1475 | args+=("$current_user") 1476 | fi 1477 | 1478 | if current_group="$(id -g -n 2> /dev/null)" && [[ -n ${current_group-} ]]; then 1479 | format+=' --polkit-allowed-group %q' 1480 | args+=("$current_group") 1481 | fi 1482 | 1483 | format+='\nPlease see %s for additional details on configuring polkit.\n' 1484 | args+=('https://github.com/tomeon/update-systemd-resolved/tree/polkit-rules-definition#policykit-rules') 1485 | 1486 | # shellcheck disable=SC2059 1487 | printf -- "$format" "${args[@]}" 1488 | } 1489 | 1490 | main() { 1491 | local action 1492 | while (("$#" > 0)); do 1493 | case "$1" in 1494 | up | down | print-polkit-rules) 1495 | action="$1" 1496 | ;; 1497 | --) 1498 | shift 1499 | break 1500 | ;; 1501 | *) 1502 | break 1503 | ;; 1504 | esac 1505 | 1506 | shift 1507 | done 1508 | 1509 | action="${action:-${script_type:-down}}" 1510 | action="${action//-/_}" 1511 | 1512 | if ! declare -f "${action}" &> /dev/null; then 1513 | usage "Invalid script type: '${action}'" 1514 | return 1 1515 | fi 1516 | 1517 | "$action" "$@" 1518 | } 1519 | 1520 | if [[ ${BASH_SOURCE[0]} == "$0" ]] || [[ ${AUTOMATED_TESTING-} == 1 ]]; then 1521 | set -o nounset 1522 | 1523 | main "$@" 1524 | fi 1525 | --------------------------------------------------------------------------------