From 5bccbe14e20d375300b2c195090152ed86a9e437 Mon Sep 17 00:00:00 2001 From: Kirill Isakov Date: Thu, 29 Jul 2021 20:45:42 +0600 Subject: [PATCH] Add tests for the fsck command. Also, allow running tests as non-root and elevate as necessary. This requires passwordless sudo and the CI envvar set to any non-empty value. --- test/Makefile.am | 10 +- test/command-fsck.test | 290 +++++++++++++++++++++++++++++++++++++++++ test/compression.test | 5 +- test/ns-ping.test | 4 +- test/testlib.sh.in | 102 ++++++++++++++- 5 files changed, 397 insertions(+), 14 deletions(-) create mode 100755 test/command-fsck.test diff --git a/test/Makefile.am b/test/Makefile.am index da77fb79..20fee657 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -2,7 +2,7 @@ TESTS = \ basic.test \ executables.test \ commandline.test \ - compression.test \ + command-fsck.test \ import-export.test \ invite-join.test \ invite-offline.test \ @@ -14,12 +14,14 @@ TESTS = \ if WITH_LEGACY_PROTOCOL TESTS += \ - legacy-protocol.test \ - algorithms.test + legacy-protocol.test \ + algorithms.test endif if LINUX -TESTS += ns-ping.test +TESTS += \ + ns-ping.test \ + compression.test endif dist_check_SCRIPTS = $(TESTS) diff --git a/test/command-fsck.test b/test/command-fsck.test new file mode 100755 index 00000000..8457baa2 --- /dev/null +++ b/test/command-fsck.test @@ -0,0 +1,290 @@ +#!/bin/sh + +. ./testlib.sh + +foo_dir=$(peer_directory foo) +foo_host=$foo_dir/hosts/foo +foo_conf=$foo_dir/tinc.conf +foo_rsa_priv=$foo_dir/rsa_key.priv +foo_ec_priv=$foo_dir/ed25519_key.priv +foo_tinc_up=$foo_dir/tinc-up +foo_host_up=$foo_dir/host-up + +if is_windows; then + foo_tinc_up=$foo_tinc_up.cmd + foo_host_up=$foo_host_up.cmd +fi + +# Sample RSA key pair (old format). Uses e = 0xFFFF. +rsa_n=BB82C3A9B906E98ABF2D99FF9B320B229F5C1E58EC784762DA1F4D3509FFF78ECA7FFF19BA170736CDE458EC8E732DDE2C02009632DF731B4A6BD6C504E50B7B875484506AC1E49FD0DF624F6612F564C562BD20F870592A49195023D744963229C35081C8AE48BE2EBB5CC9A0D64924022DC0EB782A3A8F3EABCA04AA42B24B2A6BD2353A6893A73AE01FA54891DD24BF36CA032F19F7E78C01273334BAA2ECF36B6998754CB012BC985C975503D945E4D925F6F719ACC8FBA7B18C810FF850C3CCACD60565D4FCFE02A98FE793E2D45D481A34D1F90584D096561FF3184C462C606535F3F9BB260541DF0D1FEB16938FFDEC2FF96ACCC6BD5BFBC19471F6AB +rsa_d=8CEC9A4316FE45E07900197D8FBB52D3AF01A51C4F8BD08A1E21A662E3CFCF7792AD7680673817B70AC1888A08B49E8C5835357016D9BF56A0EBDE8B5DF214EC422809BC8D88177F273419116EF2EC7951453F129768DE9BC31D963515CC7481559E4C0E65C549169F2B94AE68DB944171189DD654DC6970F2F5843FB7C8E9D057E2B5716752F1F5686811AC075ED3D3CBD06B5D35AE33D01260D9E0560AF545D0C9D89A31D5EAF96D5422F6567FE8A90E23906B840545805644DFD656E526A686D3B978DD271578CA3DA0F7D23FC1252A702A5D597CAE9D4A5BBF6398A75AF72582C7538A7937FB71A2610DCBC39625B77103FA3B7D0A55177FD98C39CD4A27 + +# Extracts the PEM key from a config file, leaving the file unchanged. +# usage: extract_pem_key_from_config path_to_file +extract_pem_key_from_config() { + sed -n '/-----BEGIN /,/-----END /p' "$1" +} + +# Removes the PEM key from a config file. +# usage: rm_pem_key_from_config path_to_file +rm_pem_key_from_config() { + sed_cmd '/-----BEGIN /,/-----END /d' "$1" +} + +reinit_configs() { + if [ -d "$foo_dir" ]; then + chmod -f 755 "$foo_dir" + rm -rf "$foo_dir" + fi + + tinc foo <&2 "[STEP] $*" + reinit_configs +} + +run_access_checks() { + ! is_root && ! is_windows +} + +test_private_keys() { + keyfile=$1 + + fsck_test "Must fail on broken $keyfile" + printf '' >"$foo_dir/$keyfile" + if with_legacy; then + expect_msg 'no private key is known' tinc foo fsck + else + must_fail_with_msg 'no Ed25519 private key found' tinc foo fsck + fi + + if run_access_checks; then + fsck_test "Must fail on inaccessible $keyfile" + chmod 000 "$foo_dir/$keyfile" + if with_legacy; then + expect_msg 'error reading' tinc foo fsck + else + must_fail_with_msg 'error reading' tinc foo fsck + fi + fi + + if ! is_windows; then + fsck_test "Must warn about unsafe permissions on $keyfile" + chmod 666 "$foo_dir/$keyfile" + expect_msg 'unsafe file permissions' tinc foo fsck + fi + + if with_legacy; then + fsck_test "Must pass on missing $keyfile when the other key is present" + rm -f "$foo_dir/$keyfile" + tinc foo fsck + fi +} + +test_private_key_var() { + var=$1 + keyfile=$2 + + fsck_test "Must find private key at $var" + mv "$foo_dir/$keyfile" "$foo_dir/renamed_private_key" + echo "$var = $(normalize_path "$foo_dir/renamed_private_key")" >>"$foo_conf" + fail_on_msg 'key was found but no private key' tinc foo fsck +} + +test_ec_public_key_file_var() { + conf=$1 + fsck_test "EC public key in Ed25519PublicKeyFile in $conf must work" + cat >"$foo_dir/ec_pubkey" <>"$foo_dir/$conf" + fail_on_msg 'no (usable) public Ed25519' tinc foo fsck +} + +test_rsa_public_key_file_var() { + conf=$1 + fsck_test "RSA public key in PublicKeyFile in $conf must work" + extract_pem_key_from_config "$foo_host" >"$foo_dir/rsa_pubkey" + rm_pem_key_from_config "$foo_host" + echo "PublicKeyFile = $(normalize_path "$foo_dir/rsa_pubkey")" >>"$foo_dir/$conf" + fail_on_msg 'error reading RSA public key' tinc foo fsck +} + +fsck_test 'Newly created configuration should pass' +tinc foo fsck + +fsck_test 'Must fail on missing tinc.conf' +rm -f "$foo_conf" +must_fail_with_msg 'no tinc configuration found' tinc foo fsck + +if run_access_checks; then + fsck_test 'Must fail on inaccessible tinc.conf' + chmod 000 "$foo_dir" + must_fail_with_msg 'not running tinc as root' tinc foo fsck +fi + +if ! is_windows; then + fsck_test 'Non-executable tinc-up MUST be fixed by tinc --force' + chmod a-x "$foo_tinc_up" + expect_msg 'cannot read and execute' tinc foo --force fsck + test -x "$foo_tinc_up" + + fsck_test 'Non-executable tinc-up MUST NOT be fixed by tinc without --force' + chmod a-x "$foo_tinc_up" + expect_msg 'cannot read and execute' tinc foo fsck + must_fail test -x "$foo_tinc_up" +fi + +fsck_test 'Unknown -up script warning' +touch "$foo_dir/fake-up" +expect_msg 'unknown script' tinc foo fsck + +fsck_test 'Unknown -down script warning' +touch "$foo_dir/fake-down" +expect_msg 'unknown script' tinc foo fsck + +if ! is_windows; then + fsck_test 'Non-executable foo-up MUST be fixed by tinc --force' + touch "$foo_host_up" + chmod a-x "$foo_host_up" + expect_msg 'cannot read and execute' tinc foo --force fsck + test -x "$foo_tinc_up" + + fsck_test 'Non-executable bar-up MUST NOT be fixed by tinc' + touch "$foo_dir/hosts/bar-up" + chmod a-x "$foo_dir/hosts/bar-up" + expect_msg 'cannot read and execute' tinc foo fsck + must_fail test -x "$foo_dir/bar-up" +fi + +if run_access_checks; then + fsck_test 'Inaccessible hosts/foo must fail' + chmod 000 "$foo_host" + must_fail_with_msg 'cannot open config file' tinc foo fsck +fi + +fsck_test 'Must fail when all private keys are missing' +rm -f "$foo_ec_priv" "$foo_rsa_priv" +if with_legacy; then + must_fail_with_msg 'neither RSA or Ed25519 private key' tinc foo fsck +else + must_fail_with_msg 'no Ed25519 private key' tinc foo fsck +fi + +if with_legacy; then + test_private_keys rsa_key.priv + + if ! is_windows; then + fsck_test 'Must warn about unsafe permissions on tinc.conf with PrivateKey' + rm -f "$foo_rsa_priv" + echo "PrivateKey = $rsa_d" >>"$foo_conf" + echo "PublicKey = $rsa_n" >>"$foo_host" + chmod 666 "$foo_conf" + expect_msg 'unsafe file permissions' tinc foo fsck + fi + + fsck_test 'Must warn about missing RSA private key if public key is present' + rm -f "$foo_rsa_priv" + expect_msg 'public RSA key was found but no private key' tinc foo fsck + + fsck_test 'Must warn about missing RSA public key' + rm_pem_key_from_config "$foo_host" + expect_msg 'no (usable) public RSA' tinc foo fsck + must_fail grep -q 'BEGIN RSA PUBLIC KEY' "$foo_host" + + fsck_test 'Must fix missing RSA public key on --force' + rm_pem_key_from_config "$foo_host" + expect_msg 'wrote RSA public key' tinc foo --force fsck + grep -q 'BEGIN RSA PUBLIC KEY' "$foo_host" + + test_private_key_var PrivateKeyFile rsa_key.priv + + test_rsa_public_key_file_var tinc.conf + test_rsa_public_key_file_var hosts/foo + + fsck_test 'RSA PublicKey + PrivateKey must work' + rm -f "$foo_rsa_priv" + rm_pem_key_from_config "$foo_host" + echo "PrivateKey = $rsa_d" >>"$foo_conf" + echo "PublicKey = $rsa_n" >>"$foo_host" + fail_on_msg 'no (usable) public RSA' tinc foo fsck + + fsck_test 'RSA PrivateKey without PublicKey must warn' + rm -f "$foo_rsa_priv" + rm_pem_key_from_config "$foo_host" + echo "PrivateKey = $rsa_d" >>"$foo_conf" + expect_msg 'PrivateKey used but no PublicKey found' tinc foo fsck + + fsck_test 'Must warn about missing EC private key if public key is present' + rm -f "$foo_ec_priv" + expect_msg 'public Ed25519 key was found but no private key' tinc foo fsck + + fsck_test 'Must fix broken RSA public key with --force' + sed_cmd 2d "$foo_host" + expect_msg 'old key(s) found and disabled' tinc foo --force fsck + tinc foo fsck + + fsck_test 'Must fix missing RSA public key with --force' + rm_pem_key_from_config "$foo_host" + expect_msg 'no (usable) public RSA key found' tinc foo --force fsck + tinc foo fsck +fi + +fsck_test 'Must fix broken Ed25519 public key with --force' +sed_cmd 's/Ed25519PublicKey.*/Ed25519PublicKey = foobar/' "$foo_host" +expect_msg 'no (usable) public Ed25519 key' tinc foo --force fsck +tinc foo fsck + +fsck_test 'Must fix missing Ed25519 public key with --force' +sed_cmd '/Ed25519PublicKey/d' "$foo_host" +expect_msg 'no (usable) public Ed25519 key' tinc foo --force fsck +tinc foo fsck + +test_private_keys ed25519_key.priv +test_private_key_var Ed25519PrivateKeyFile ed25519_key.priv + +test_ec_public_key_file_var tinc.conf +test_ec_public_key_file_var hosts/foo + +fsck_test 'Must warn about missing EC public key and NOT fix without --force' +sed_cmd '/Ed25519PublicKey/d' "$foo_host" +expect_msg 'no (usable) public Ed25519' tinc foo fsck +must_fail grep -q 'ED25519 PUBLIC KEY' "$foo_host" + +fsck_test 'Must fix missing EC public key on --force' +sed_cmd '/Ed25519PublicKey/d' "$foo_host" +expect_msg 'wrote Ed25519 public key' tinc foo --force fsck +grep -q 'ED25519 PUBLIC KEY' "$foo_host" + +fsck_test 'Must warn about obsolete variables' +echo 'GraphDumpFile = /dev/null' >>"$foo_host" +expect_msg 'obsolete variable GraphDumpFile' tinc foo fsck + +fsck_test 'Must warn about missing values' +echo 'Weight = ' >>"$foo_host" +must_fail_with_msg 'no value for variable `Weight' tinc foo fsck + +fsck_test 'Must warn about duplicate variables' +echo 'Weight = 0' >>"$foo_host" +echo 'Weight = 1' >>"$foo_host" +expect_msg 'multiple instances of variable Weight' tinc foo fsck + +fsck_test 'Must warn about server variables in host config' +echo 'Interface = fake0' >>"$foo_host" +expect_msg 'server variable Interface found' tinc foo fsck + +fsck_test 'Must warn about host variables in server config' +echo 'Port = 1337' >>"$foo_conf" +expect_msg 'host variable Port found' tinc foo fsck + +fsck_test 'Must warn about missing Name' +sed_cmd '/^Name =/d' "$foo_conf" +must_fail_with_msg 'without a valid Name' tinc foo fsck diff --git a/test/compression.test b/test/compression.test index 0572e48d..7c69c2ca 100755 --- a/test/compression.test +++ b/test/compression.test @@ -2,7 +2,7 @@ . ./testlib.sh -test "$(id -u)" = "0" || exit $EXIT_SKIP_TEST +require_root "$0" "$@" test -e /dev/net/tun || exit $EXIT_SKIP_TEST ip netns list || exit $EXIT_SKIP_TEST command -v socat || exit $EXIT_SKIP_TEST @@ -110,8 +110,9 @@ for level in $levels; do socat -u TCP4-LISTEN:$recv_port_foo,reuseaddr OPEN:"$tmp_file",creat & ip netns exec bar \ - socat -u OPEN:"$ref_file" TCP4:$ip_foo:$recv_port_foo,retry=30 + socat -u OPEN:"$ref_file" TCP4:$ip_foo:$recv_port_foo,retry=30 & + wait diff -w "$ref_file" "$tmp_file" tinc foo stop diff --git a/test/ns-ping.test b/test/ns-ping.test index bb76ed39..8e10d11e 100755 --- a/test/ns-ping.test +++ b/test/ns-ping.test @@ -2,9 +2,7 @@ . ./testlib.sh -echo "[STEP] Skip this test if we aren't root or if 'ip netns' does not exist" - -test "$(id -u)" = "0" || exit $EXIT_SKIP_TEST +require_root "$0" "$@" test -e /dev/net/tun || exit $EXIT_SKIP_TEST ip netns list || exit $EXIT_SKIP_TEST diff --git a/test/testlib.sh.in b/test/testlib.sh.in index ec8f400f..c65bdde8 100644 --- a/test/testlib.sh.in +++ b/test/testlib.sh.in @@ -58,6 +58,12 @@ if type gtimeout >/dev/null; then timeout() { gtimeout "$@"; } fi +# As usual, BSD tools require special handling, as they do not support -i without a suffix. +# Note that there must be no space after -i, or it won't work on GNU sed. +sed_cmd() { + sed -i.orig "$@" +} + # Are the shell tools provided by busybox? is_busybox() { timeout --help 2>&1 | grep -q -i busybox @@ -93,6 +99,12 @@ rm_cr() { tr -d '\r' } +if is_windows; then + normalize_path() { cygpath --mixed -- "$@"; } +else + normalize_path() { echo "$@"; } +fi + # Executes whatever is passed to it, checking that the resulting exit code is non-zero. must_fail() { if "$@"; then @@ -100,6 +112,63 @@ must_fail() { fi } +# Executes the passed command and checks two conditions: +# 1. it must exit successfully (with code 0) +# 2. its output (stdout + stderr) must include the substring from the first argument (ignoring case) +# usage: expect_msg 'expected message' command --with --args +expect_msg() { + message=$1 + shift + + if ! output=$("$@" 2>&1); then + bail 'expected 0 exit code' + fi + + if ! echo "$output" | grep -q -i "$message"; then + bail "expected message '$message'" + fi +} + +# The reverse of expect_msg. We cannot simply wrap expect_msg with must_fail +# because there should be a separate check for tinc exit code. +fail_on_msg() { + message=$1 + shift + + if ! output=$("$@" 2>&1); then + bail 'expected 0 exit code' + fi + + if echo "$output" | grep -q -i "$message"; then + bail "unexpected message '$message'" + fi +} + +# Like expect_msg, but the command must fail with a non-zero exit code. +# usage: must_fail_with_msg 'expected message' command --with --args +must_fail_with_msg() { + message=$1 + shift + + if output=$("$@" 2>&1); then + bail "expected a non-zero exit code" + fi + + if ! echo "$output" | grep -i -q "$message"; then + bail "expected message '$message'" + fi +} + +# Is the legacy protocol enabled? +with_legacy() { + tincd foo --version | grep -q legacy_protocol +} + +# Are we running with EUID 0? +is_root() { + test "$(id -u)" = 0 +} + # Executes whatever is passed to it, checking that the resulting exit code is equal to the first argument. expect_code() { expected=$1 @@ -215,6 +284,7 @@ require_nodes() { } peer_directory() { + peer=$1 case "$peer" in foo) echo "$DIR_FOO" ;; bar) echo "$DIR_BAR" ;; @@ -370,9 +440,32 @@ cleanup() { ) || true } +# If we're on a CI server, the test requires superuser privileges to run, and we're not +# currently a superuser, try running the test as one and fail if it doesn't work (the +# system must be configured to provide passwordless sudo for our user). +require_root() { + if is_root; then + return + fi + if is_ci; then + echo "root is required for test $SCRIPTNAME, but we're a regular user; elevating privileges..." + if ! command -v sudo 2>/dev/null; then + bail "please install sudo and configure passwordless auth for user $USER" + fi + if ! sudo --preserve-env --non-interactive true; then + bail "sudo is not allowed or requires a password for user $USER" + fi + exec sudo --preserve-env "$@" + else + # Avoid these kinds of surprises outside CI. Just skip the test. + echo "root is required for test $SCRIPTNAME, but we're a regular user; skipping" + exit $EXIT_SKIP_TEST + fi +} + # Generate path to current shell which can be used from Windows applications. if is_windows; then - MINGW_SHELL=$(cygpath --mixed -- "$SHELL") + MINGW_SHELL=$(normalize_path "$SHELL") fi # This was called from a tincd script. Skip executing commands with side effects. @@ -383,10 +476,9 @@ echo [STEP] Check for leftover tinc daemons and test directories # Cleanup leftovers from previous runs. stop_all_tincs -# On Windows this can actually fail. We don't want to suppress possible failure with -f. -if [ -d "$DIR_FOO" ]; then rm -r "$DIR_FOO"; fi -if [ -d "$DIR_BAR" ]; then rm -r "$DIR_BAR"; fi -if [ -d "$DIR_BAZ" ]; then rm -r "$DIR_BAZ"; fi +if [ -d "$DIR_FOO" ]; then rm -rf "$DIR_FOO"; fi +if [ -d "$DIR_BAR" ]; then rm -rf "$DIR_BAR"; fi +if [ -d "$DIR_BAZ" ]; then rm -rf "$DIR_BAZ"; fi # Register cleanup function so we don't have to call it everywhere # (and failed scripts do not leave stray tincd running). -- 2.20.1