Add tests for the fsck command.
authorKirill Isakov <is-kir@ya.ru>
Thu, 29 Jul 2021 14:45:42 +0000 (20:45 +0600)
committerKirill Isakov <is-kir@ya.ru>
Thu, 29 Jul 2021 14:45:42 +0000 (20:45 +0600)
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
test/command-fsck.test [new file with mode: 0755]
test/compression.test
test/ns-ping.test
test/testlib.sh.in

index da77fb7..20fee65 100644 (file)
@@ -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 (executable)
index 0000000..8457baa
--- /dev/null
@@ -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 <<EOF
+init foo
+set DeviceType dummy
+EOF
+}
+
+fsck_test() {
+  echo >&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" <<EOF
+-----BEGIN ED25519 PUBLIC KEY-----
+$(awk '/^Ed25519PublicKey/ { printf $NF }' "$foo_host")
+-----END ED25519 PUBLIC KEY-----
+EOF
+  sed_cmd '/Ed25519PublicKey/d' "$foo_host"
+  echo "Ed25519PublicKeyFile = $(normalize_path "$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
index 0572e48..7c69c2c 100755 (executable)
@@ -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
index bb76ed3..8e10d11 100755 (executable)
@@ -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
 
index ec8f400..c65bdde 100644 (file)
@@ -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).