├── .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 | [](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 |
--------------------------------------------------------------------------------