diff --git a/.github/actions/apt-x64/action.yml b/.github/actions/apt-x64/action.yml index 3e654ef9fdc4..71b239920737 100644 --- a/.github/actions/apt-x64/action.yml +++ b/.github/actions/apt-x64/action.yml @@ -33,6 +33,9 @@ runs: ldap-utils \ openssl \ slapd \ + bind9 \ + bind9utils \ + bind9-dnsutils \ language-pack-de \ libgmp-dev \ libicu-dev \ diff --git a/.github/actions/setup-x64/action.yml b/.github/actions/setup-x64/action.yml index 3ee5cd3f28f2..021a63279eaa 100644 --- a/.github/actions/setup-x64/action.yml +++ b/.github/actions/setup-x64/action.yml @@ -6,6 +6,10 @@ runs: run: | set -x + sudo systemctl stop named + sudo ./ext/standard/tests/dns/bind-start.sh + sudo ./ext/standard/tests/dns/resolv-setup.sh + sudo service slapd start docker exec sql1 /opt/mssql-tools18/bin/sqlcmd -S 127.0.0.1 -U SA -C -P "" -Q "create login pdo_test with password='password', check_policy=off; create user pdo_test for login pdo_test; grant alter, control to pdo_test;" docker exec sql1 /opt/mssql-tools18/bin/sqlcmd -S 127.0.0.1 -U SA -C -P "" -Q "create login odbc_test with password='password', check_policy=off; create user odbc_test for login odbc_test; grant alter, control, delete to odbc_test;" diff --git a/ext/standard/tests/dns/.gitignore b/ext/standard/tests/dns/.gitignore new file mode 100644 index 000000000000..84fd15724d32 --- /dev/null +++ b/ext/standard/tests/dns/.gitignore @@ -0,0 +1,6 @@ +!*.sh +named.conf +named.pid +named.log +managed-keys.* +*.jnl diff --git a/ext/standard/tests/dns/README.md b/ext/standard/tests/dns/README.md new file mode 100644 index 000000000000..6c28e63f51d4 --- /dev/null +++ b/ext/standard/tests/dns/README.md @@ -0,0 +1,41 @@ +# DNS tests + +These tests run the PHP DNS functions against a local BIND 9 server serving +the zones from `zones/`. They are skipped unless the server is running and +`/etc/resolv.conf` points to it. + +Requirements on Debian / Ubuntu: + +```sh +sudo apt-get install bind9 bind9utils bind9-dnsutils +``` + +Nothing else may listen on `127.0.0.1:53` (the system `named` service started +by the package install needs to be stopped). systemd-resolved listens on +`127.0.0.53` and is not a problem. + +Running the tests: + +```sh +sudo ext/standard/tests/dns/bind-start.sh +sudo ext/standard/tests/dns/resolv-setup.sh + +sapi/cli/php run-tests.php ext/standard/tests/dns + +sudo ext/standard/tests/dns/resolv-reset.sh +sudo ext/standard/tests/dns/bind-stop.sh +``` + +`bind-start.sh` generates `named.conf` from `named.conf.in` and starts `named` +on `127.0.0.1:53` as the owner of this directory. Other queries are forwarded +to the nameservers the host uses so everything else keeps resolving. The +server logs to `named.log`. + +`resolv-setup.sh` saves `/etc/resolv.conf` and replaces it with one using +`127.0.0.1` with the previous nameservers as a fallback. The PHP DNS +functions use libresolv which reads this file directly, so this is the only +setting that matters. systemd-resolved is not used on purpose as it re-applies +DHCP servers and routes queries per interface. + +To add records, edit `zones/basic.dnstest.php.net.zone` and bump the serial, +or add a new zone file and a `zone` block to `named.conf.in`. diff --git a/ext/standard/tests/dns/bind-start.sh b/ext/standard/tests/dns/bind-start.sh new file mode 100755 index 000000000000..10d59087a4a0 --- /dev/null +++ b/ext/standard/tests/dns/bind-start.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Starts BIND serving the zones from zones/ on 127.0.0.1:53, forwarding +# everything else to the nameservers the host uses. Use -f to run it in +# the foreground. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +command -v named >/dev/null || fail "named not found, install BIND 9 (bind9 bind9utils bind9-dnsutils)" + +if pid="$(running_pid)"; then + echo "BIND is already running with PID $pid" + exit 0 +fi + +# Installing bind9 on Debian/Ubuntu starts the system named on port 53 +if command -v ss >/dev/null; then + listener="$(ss -H -uln "sport = :$DNS_PORT" | awk -v a="$DNS_ADDRESS:$DNS_PORT" -v p="$DNS_PORT" \ + '$4 == a || $4 == "0.0.0.0:" p || $4 == "[::]:" p || $4 == "*:" p { print $4; exit }')" + [[ -z "$listener" ]] || fail "$listener is already in use, stop the service using it first (e.g. systemctl stop named)" +fi + +forwarders="" +while read -r ns; do + forwarders+="$ns; " +done < <(upstream_nameservers) +if [[ -n "$forwarders" ]]; then + forwarders="forwarders { $forwarders}; forward first;" +fi + +sed -e "s|@TEST_DIR@|$TEST_DIR|g" \ + -e "s|@PID_FILE@|$PID_FILE|g" \ + -e "s|@LOG_FILE@|$LOG_FILE|g" \ + -e "s|@ADDRESS@|$DNS_ADDRESS|g" \ + -e "s|@PORT@|$DNS_PORT|g" \ + -e "s|@FORWARDERS@|$forwarders|" \ + "$TEST_DIR/named.conf.in" > "$NAMED_CONF" +chmod 644 "$NAMED_CONF" + +if command -v named-checkconf >/dev/null; then + named-checkconf -z "$NAMED_CONF" >/dev/null +fi + +# Drop privileges to the owner of this directory so named can write here +args=(-c "$NAMED_CONF") +owner="" +if [[ "$(id -u)" -eq 0 ]]; then + owner="$(stat -c '%U' "$TEST_DIR")" + if [[ "$owner" != "root" ]]; then + args+=(-u "$owner") + fi +elif [[ "$DNS_PORT" -lt 1024 ]]; then + fail "must run as root to listen on port $DNS_PORT" +fi + +# The distribution profile only allows named to read /etc/bind and /var/{cache,lib}/bind +if can_manage_apparmor; then + apparmor_parser -R "$AA_PROFILE" 2>/dev/null || true +fi + +rm -f "${PID_FILE:?}" "${ZONES_DIR:?}"/*.jnl +: > "$LOG_FILE" +[[ -z "$owner" ]] || chown "$owner" "$LOG_FILE" + +if [[ "${1:-}" == "-f" ]]; then + exec named "${args[@]}" -g +fi + +named "${args[@]}" + +for _ in $(seq 1 40); do + if pid="$(running_pid)"; then + if ! command -v dig >/dev/null \ + || [[ "$(dig "@$DNS_ADDRESS" -p "$DNS_PORT" +short +time=1 +tries=1 www.basic.dnstest.php.net A)" == "192.0.2.1" ]]; then + echo "BIND started with PID $pid on $DNS_ADDRESS:$DNS_PORT" + exit 0 + fi + fi + sleep 0.25 +done + +cat "$LOG_FILE" >&2 +fail "BIND did not start, see $LOG_FILE" diff --git a/ext/standard/tests/dns/bind-stop.sh b/ext/standard/tests/dns/bind-stop.sh new file mode 100755 index 000000000000..ca04de4195ca --- /dev/null +++ b/ext/standard/tests/dns/bind-stop.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Stops BIND started by bind-start.sh + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +if pid="$(running_pid)"; then + kill "$pid" + for _ in $(seq 1 40); do + kill -0 "$pid" 2>/dev/null || break + sleep 0.25 + done + echo "BIND with PID $pid stopped" +else + echo "BIND is not running" +fi + +rm -f "${PID_FILE:?}" "${NAMED_CONF:?}" "${ZONES_DIR:?}"/*.jnl + +if can_manage_apparmor; then + apparmor_parser -r "$AA_PROFILE" 2>/dev/null || true +fi diff --git a/ext/standard/tests/dns/checkdnsrr_basic.phpt b/ext/standard/tests/dns/checkdnsrr_basic.phpt new file mode 100644 index 000000000000..06f6f2a806ad --- /dev/null +++ b/ext/standard/tests/dns/checkdnsrr_basic.phpt @@ -0,0 +1,22 @@ +--TEST-- +checkdnsrr() and dns_check_record() basic usage +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) diff --git a/ext/standard/tests/dns/common.sh b/ext/standard/tests/dns/common.sh new file mode 100755 index 000000000000..32b6080ac942 --- /dev/null +++ b/ext/standard/tests/dns/common.sh @@ -0,0 +1,49 @@ +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ZONES_DIR="$TEST_DIR/zones" +NAMED_CONF="$TEST_DIR/named.conf" +PID_FILE="$TEST_DIR/named.pid" +LOG_FILE="$TEST_DIR/named.log" + +DNS_ADDRESS="${PHP_DNS_TEST_ADDRESS:-127.0.0.1}" +DNS_PORT="${PHP_DNS_TEST_PORT:-53}" + +RESOLV_CONF="/etc/resolv.conf" +RESOLV_CONF_BACKUP="/etc/resolv.conf.php-dns-test.orig" + +AA_PROFILE="/etc/apparmor.d/usr.sbin.named" + +fail() { + echo "$*" >&2 + exit 1 +} + +resolv_nameservers() { + [[ -r "$1" ]] && sed -nE 's/^[[:space:]]*nameserver[[:space:]]+([^[:space:]#]+).*/\1/p' "$1" + return 0 +} + +# Nameservers the host uses, skipping loopback (the systemd-resolved stub or our own server) +upstream_nameservers() { + local f ns found=0 + for f in "$RESOLV_CONF_BACKUP" /run/systemd/resolve/resolv.conf "$RESOLV_CONF"; do + while read -r ns; do + case "$ns" in + 127.*|::1) ;; + *) echo "$ns"; found=1 ;; + esac + done < <(resolv_nameservers "$f") + [[ $found -eq 1 ]] && return 0 + done +} + +running_pid() { + local pid + pid="$(cat "$PID_FILE" 2>/dev/null)" || return 1 + [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null || return 1 + echo "$pid" +} + +can_manage_apparmor() { + [[ -f "$AA_PROFILE" && -d /sys/kernel/security/apparmor && "$(id -u)" -eq 0 ]] \ + && command -v apparmor_parser >/dev/null +} diff --git a/ext/standard/tests/dns/dns_get_record_basic.phpt b/ext/standard/tests/dns/dns_get_record_basic.phpt new file mode 100644 index 000000000000..5babd5d601b9 --- /dev/null +++ b/ext/standard/tests/dns/dns_get_record_basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +dns_get_record() basic usage with A record +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +array(1) { + [0]=> + array(5) { + ["host"]=> + string(25) "www.basic.dnstest.php.net" + ["class"]=> + string(2) "IN" + ["ttl"]=> + int(%d) + ["type"]=> + string(1) "A" + ["ip"]=> + string(9) "192.0.2.1" + } +} diff --git a/ext/standard/tests/dns/dns_get_record_mx.phpt b/ext/standard/tests/dns/dns_get_record_mx.phpt new file mode 100644 index 000000000000..fe320acf9130 --- /dev/null +++ b/ext/standard/tests/dns/dns_get_record_mx.phpt @@ -0,0 +1,29 @@ +--TEST-- +dns_get_record() with MX record +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +array(1) { + [0]=> + array(6) { + ["host"]=> + string(21) "basic.dnstest.php.net" + ["class"]=> + string(2) "IN" + ["ttl"]=> + int(%d) + ["type"]=> + string(2) "MX" + ["pri"]=> + int(10) + ["target"]=> + string(25) "mx1.basic.dnstest.php.net" + } +} diff --git a/ext/standard/tests/dns/dns_get_record_nonexistent.phpt b/ext/standard/tests/dns/dns_get_record_nonexistent.phpt new file mode 100644 index 000000000000..f5b2d934b584 --- /dev/null +++ b/ext/standard/tests/dns/dns_get_record_nonexistent.phpt @@ -0,0 +1,16 @@ +--TEST-- +dns_get_record() with a name that does not exist +--SKIPIF-- + +--FILE-- + +--EXPECT-- +array(0) { +} +array(0) { +} diff --git a/ext/standard/tests/dns/dns_get_record_txt.phpt b/ext/standard/tests/dns/dns_get_record_txt.phpt new file mode 100644 index 000000000000..6fa2bea865a6 --- /dev/null +++ b/ext/standard/tests/dns/dns_get_record_txt.phpt @@ -0,0 +1,32 @@ +--TEST-- +dns_get_record() with TXT record +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +array(1) { + [0]=> + array(6) { + ["host"]=> + string(26) "txt1.basic.dnstest.php.net" + ["class"]=> + string(2) "IN" + ["ttl"]=> + int(%d) + ["type"]=> + string(3) "TXT" + ["txt"]=> + string(25) "This is a test TXT record" + ["entries"]=> + array(1) { + [0]=> + string(25) "This is a test TXT record" + } + } +} diff --git a/ext/standard/tests/dns/getmxrr_basic.phpt b/ext/standard/tests/dns/getmxrr_basic.phpt new file mode 100644 index 000000000000..d77ec5ad5598 --- /dev/null +++ b/ext/standard/tests/dns/getmxrr_basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +getmxrr() basic usage +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +array(1) { + [0]=> + string(25) "mx1.basic.dnstest.php.net" +} +array(1) { + [0]=> + int(10) +} +bool(false) +array(0) { +} +array(0) { +} diff --git a/ext/standard/tests/dns/named.conf.in b/ext/standard/tests/dns/named.conf.in new file mode 100644 index 000000000000..e446431ff307 --- /dev/null +++ b/ext/standard/tests/dns/named.conf.in @@ -0,0 +1,29 @@ +options { + directory "@TEST_DIR@"; + pid-file "@PID_FILE@"; + listen-on port @PORT@ { @ADDRESS@; }; + listen-on-v6 { none; }; + allow-query { any; }; + recursion yes; + allow-recursion { localhost; }; + dnssec-validation no; + @FORWARDERS@ +}; + +controls { }; + +logging { + channel file { + file "@LOG_FILE@"; + severity info; + print-time yes; + print-category yes; + }; + category default { file; }; + category queries { file; }; +}; + +zone "basic.dnstest.php.net" { + type primary; + file "zones/basic.dnstest.php.net.zone"; +}; diff --git a/ext/standard/tests/dns/resolv-reset.sh b/ext/standard/tests/dns/resolv-reset.sh new file mode 100755 index 000000000000..5a4be74648b9 --- /dev/null +++ b/ext/standard/tests/dns/resolv-reset.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Restores /etc/resolv.conf saved by resolv-setup.sh + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +[[ "$(id -u)" -eq 0 ]] || fail "must run as root" + +if [[ ! -e "$RESOLV_CONF_BACKUP" && ! -L "$RESOLV_CONF_BACKUP" ]]; then + echo "$RESOLV_CONF_BACKUP not found, nothing to restore" + exit 0 +fi + +if [[ -L "$RESOLV_CONF_BACKUP" ]]; then + rm -f "$RESOLV_CONF" + mv "$RESOLV_CONF_BACKUP" "$RESOLV_CONF" +else + cat "$RESOLV_CONF_BACKUP" > "$RESOLV_CONF" + rm -f "$RESOLV_CONF_BACKUP" +fi + +echo "$RESOLV_CONF restored" diff --git a/ext/standard/tests/dns/resolv-setup.sh b/ext/standard/tests/dns/resolv-setup.sh new file mode 100755 index 000000000000..e72357459a54 --- /dev/null +++ b/ext/standard/tests/dns/resolv-setup.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Points /etc/resolv.conf to the BIND started by bind-start.sh. The PHP DNS +# functions use libresolv which reads this file directly, so it is replaced +# instead of configuring systemd-resolved or NetworkManager. The current +# nameservers are kept as a fallback and resolv-reset.sh restores the file. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +[[ "$(id -u)" -eq 0 ]] || fail "must run as root" +[[ "$DNS_PORT" == "53" ]] || fail "resolv.conf cannot use port $DNS_PORT" + +if [[ -e "$RESOLV_CONF_BACKUP" || -L "$RESOLV_CONF_BACKUP" ]]; then + echo "$RESOLV_CONF_BACKUP exists, already set up" + exit 0 +fi + +content="nameserver $DNS_ADDRESS"$'\n' +while read -r ns; do + [[ "$ns" == "$DNS_ADDRESS" ]] || content+="nameserver $ns"$'\n' +done < <(resolv_nameservers "$RESOLV_CONF" | head -n 2) + +# In a container /etc/resolv.conf is a bind mount that can only be written in place +cp -P "$RESOLV_CONF" "$RESOLV_CONF_BACKUP" +if [[ -L "$RESOLV_CONF" ]]; then + rm -f "$RESOLV_CONF" +fi +printf '%s' "$content" > "$RESOLV_CONF" +chmod 644 "$RESOLV_CONF" + +echo "$RESOLV_CONF now contains:" +cat "$RESOLV_CONF" diff --git a/ext/standard/tests/dns/skipif.inc b/ext/standard/tests/dns/skipif.inc new file mode 100644 index 000000000000..a919cdf938f6 --- /dev/null +++ b/ext/standard/tests/dns/skipif.inc @@ -0,0 +1,14 @@ +