--- /dev/null
+[MASTER]
+jobs=0
+persistent=yes
+py-version=3.6
+recursive=yes
+ignore=build
+
+[BASIC]
+good-names=foo, bar, baz, f, k, ex
+
+[REPORTS]
+output-format=colorized
+
+[DESIGN]
+min-public-methods=0
+
+[SIMILARITIES]
+min-similarity-lines=10
+
#endif // HAVE_WINDOWS
+static void print_listening_msg(int sock) {
+ sockaddr_t sa = {0};
+ socklen_t salen = sizeof(sa);
+ int port = 0;
+
+ if(!getsockname(sock, &sa.sa, &salen)) {
+ port = ntohs(sa.in.sin_port);
+ }
+
+ fprintf(stderr, "Listening on %d...\n", port);
+ fflush(stderr);
+}
+
int main(int argc, char *argv[]) {
program_name = argv[0];
bool initiator = false;
return 1;
}
- fprintf(stderr, "Listening...\n");
+ print_listening_msg(sock);
sock = accept(sock, NULL, NULL);
return 1;
}
} else {
- fprintf(stderr, "Listening...\n");
+ print_listening_msg(sock);
char buf[65536];
struct sockaddr addr;
--- /dev/null
+#!/usr/bin/env python3
+
+"""Check that legacy protocol works with different cryptographic algorithms."""
+
+import typing as T
+
+from testlib.test import Test
+from testlib.proc import Tinc
+from testlib.log import log
+from testlib import cmd, check
+
+
+def init(ctx: Test, digest: str, cipher: str) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ set ExperimentalProtocol no
+ set Digest {digest}
+ set Cipher {cipher}
+ """
+ foo.cmd(stdin=stdin)
+ foo.start()
+
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ set ExperimentalProtocol no
+ set Digest {digest}
+ set Cipher {cipher}
+ """
+ bar.cmd(stdin=stdin)
+
+ foo.add_script(bar.script_up)
+ bar.add_script(foo.script_up)
+
+ cmd.exchange(foo, bar)
+ bar.cmd("add", "ConnectTo", foo.name)
+ bar.cmd("start")
+
+ return foo, bar
+
+
+def test(foo: Tinc, bar: Tinc) -> None:
+ """Run tests on algorithm pair."""
+ log.info("waiting for bar to come up")
+ foo[bar.script_up].wait()
+
+ log.info("waiting for foo to come up")
+ bar[foo.script_up].wait()
+
+ log.info("checking node reachability")
+ stdout, _ = foo.cmd("info", bar.name)
+ check.is_in("reachable", stdout)
+
+
+for alg_digest in "none", "sha256", "sha512":
+ for alg_cipher in "none", "aes-256-cbc":
+ with Test("compression") as context:
+ node0, node1 = init(context, alg_digest, alg_cipher)
+ test(node0, node1)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port 30070
-set Address localhost
-set ExperimentalProtocol no
-EOF
-
-tinc bar <<EOF
-init bar
-set DeviceType dummy
-set Port 0
-set ExperimentalProtocol no
-EOF
-
-create_script foo hosts/bar-up
-create_script bar hosts/foo-up
-
-echo [STEP] Exchange configuration
-
-tinc foo export | tinc bar exchange | tinc foo import
-tinc bar add ConnectTo foo
-start_tinc foo
-
-echo [STEP] Test various ciphers and digests
-
-# The full suite results in a large test matrix and it takes a lot of time to run.
-# The list was reduced to the strongest available algorithms (and "none").
-digests="none sha256 sha512"
-ciphers="none aes-256-cbc"
-
-for digest in $digests; do
- for cipher in $ciphers; do
- echo "Testing $cipher $digest"
-
- tinc bar <<EOF
-set Digest $digest
-set Cipher $cipher
-EOF
-
- start_tinc bar
- wait_script foo hosts/bar-up
- wait_script bar hosts/foo-up
-
- tinc foo info bar | grep reachable
-
- tinc bar stop
- done
-done
--- /dev/null
+#!/usr/bin/env python3
+
+"""Check that basic functionality works (tincd can be started and stopped)."""
+
+from testlib.test import Test
+from testlib.proc import Tinc
+from testlib.log import log
+from testlib.script import Script
+from testlib import check
+
+
+def init(ctx: Test) -> Tinc:
+ """Initialize new test nodes."""
+ node = ctx.node()
+ node.add_script(Script.TINC_UP)
+ stdin = f"""
+ init {node}
+ set Address localhost
+ set Port 0
+ set DeviceType dummy
+ """
+ node.cmd(stdin=stdin)
+ return node
+
+
+def test(ctx: Test, *flags: str) -> None:
+ """Run tests with flags."""
+ log.info("init new node")
+ node = init(ctx)
+
+ log.info('starting tincd with flags "%s"', " ".join(flags))
+ tincd = node.tincd(*flags)
+
+ log.info("waiting for tinc-up script")
+ node[Script.TINC_UP].wait()
+
+ log.info("stopping tincd")
+ node.cmd("stop")
+
+ log.info("checking tincd exit code")
+ check.equals(0, tincd.wait())
+
+
+with Test("foreground mode") as context:
+ test(context, "-D")
+
+with Test("background mode") as context:
+ test(context)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize and test one node
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port 0
-EOF
-
-echo [STEP] Test running in the foreground
-
-create_script foo tinc-up '
- tinc foo stop &
-'
-start_tinc foo -D
-
-echo [STEP] Test running tinc in the background
-
-create_script foo tinc-up '
- tinc foo stop &
-'
-start_tinc foo
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-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
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test 'tinc fsck' command."""
+
+import os
+import sys
+import typing as T
+
+from testlib import check
+from testlib.log import log
+from testlib.proc import Tinc, Feature
+from testlib.util import read_text, read_lines, write_lines, append_line, write_text
+
+run_legacy_checks = Feature.LEGACY_PROTOCOL in Tinc().features
+run_access_checks = os.name != "nt" and os.geteuid() != 0
+run_executability_checks = os.name != "nt"
+run_permission_checks = run_executability_checks
+
+# Sample RSA key pair (old format). Uses e = 0xFFFF.
+RSA_N = """
+BB82C3A9B906E98ABF2D99FF9B320B229F5C1E58EC784762DA1F4D3509FFF78ECA7FFF19BA17073\
+6CDE458EC8E732DDE2C02009632DF731B4A6BD6C504E50B7B875484506AC1E49FD0DF624F6612F5\
+64C562BD20F870592A49195023D744963229C35081C8AE48BE2EBB5CC9A0D64924022DC0EB782A3\
+A8F3EABCA04AA42B24B2A6BD2353A6893A73AE01FA54891DD24BF36CA032F19F7E78C01273334BA\
+A2ECF36B6998754CB012BC985C975503D945E4D925F6F719ACC8FBA7B18C810FF850C3CCACD6056\
+5D4FCFE02A98FE793E2D45D481A34D1F90584D096561FF3184C462C606535F3F9BB260541DF0D1F\
+EB16938FFDEC2FF96ACCC6BD5BFBC19471F6AB
+""".strip()
+
+RSA_D = """
+8CEC9A4316FE45E07900197D8FBB52D3AF01A51C4F8BD08A1E21A662E3CFCF7792AD7680673817B\
+70AC1888A08B49E8C5835357016D9BF56A0EBDE8B5DF214EC422809BC8D88177F273419116EF2EC\
+7951453F129768DE9BC31D963515CC7481559E4C0E65C549169F2B94AE68DB944171189DD654DC6\
+970F2F5843FB7C8E9D057E2B5716752F1F5686811AC075ED3D3CBD06B5D35AE33D01260D9E0560A\
+F545D0C9D89A31D5EAF96D5422F6567FE8A90E23906B840545805644DFD656E526A686D3B978DD2\
+71578CA3DA0F7D23FC1252A702A5D597CAE9D4A5BBF6398A75AF72582C7538A7937FB71A2610DCB\
+C39625B77103FA3B7D0A55177FD98C39CD4A27
+""".strip()
+
+
+class Context:
+ """Test context. Used to store paths to configuration files."""
+
+ def __init__(self) -> None:
+ node = Tinc()
+ node.cmd("init", node.name)
+
+ self.node = node
+ self.host = node.sub("hosts", node.name)
+ self.conf = node.sub("tinc.conf")
+ self.rsa_priv = node.sub("rsa_key.priv")
+ self.ec_priv = node.sub("ed25519_key.priv")
+ self.tinc_up = node.sub("tinc-up")
+ self.host_up = node.sub("host-up")
+
+ if os.name == "nt":
+ self.tinc_up = f"{self.tinc_up}.cmd"
+ self.host_up = f"{self.host_up}.cmd"
+
+ def expect_msg(
+ self, msg: str, force: bool = False, code: int = 1, present: bool = True
+ ) -> None:
+ """Checks that tinc output contains (or does not contain) the expected message."""
+ args = ["fsck"]
+ if force:
+ args.insert(0, "--force")
+
+ out, err = self.node.cmd(*args, code=code)
+ if present:
+ check.is_in(msg, out, err)
+ else:
+ check.not_in(msg, out, err)
+
+
+def test(msg: str) -> Context:
+ """Create test context."""
+ context = Context()
+ log.info("TEST: %s", msg)
+ return context
+
+
+def remove_pem(config: str) -> T.List[str]:
+ """Remove PEM from a config file, leaving everything else untouched."""
+ key, result = False, []
+ for line in read_lines(config):
+ if line.startswith("-----BEGIN"):
+ key = True
+ continue
+ if line.startswith("-----END"):
+ key = False
+ continue
+ if not key:
+ result.append(line)
+ write_lines(config, result)
+ return result
+
+
+def extract_pem(config: str) -> T.List[str]:
+ """Extract PEM from a config file, ignoring everything else."""
+ key = False
+ result: T.List[str] = []
+ for line in read_lines(config):
+ if line.startswith("-----BEGIN"):
+ key = True
+ continue
+ if line.startswith("-----END"):
+ return result
+ if key:
+ result.append(line)
+ raise Exception("key not found")
+
+
+def replace_line(file_path: str, prefix: str, replace: str = "") -> None:
+ """Replace lines in a file that start with the prefix."""
+ lines = read_lines(file_path)
+ lines = [replace if line.startswith(prefix) else line for line in lines]
+ write_lines(file_path, lines)
+
+
+def test_private_key_var(var: str, file: str) -> None:
+ """Test inline private keys with variable var."""
+ context = test(f"private key variable {var} in file {file}")
+ renamed = os.path.realpath(context.node.sub("renamed_key"))
+ os.rename(src=context.node.sub(file), dst=renamed)
+ append_line(context.host, f"{var} = {renamed}")
+ context.expect_msg("key was found but no private key", present=False, code=0)
+
+
+def test_private_keys(keyfile: str) -> None:
+ """Test private keys in file keyfile."""
+ context = test(f"fail on broken {keyfile}")
+ keyfile_path = context.node.sub(keyfile)
+ os.truncate(keyfile_path, 0)
+
+ if run_legacy_checks:
+ context.expect_msg("no private key is known", code=0)
+ else:
+ context.expect_msg("No Ed25519 private key found")
+
+ if run_access_checks:
+ context = test(f"fail on inaccessible {keyfile}")
+ keyfile_path = context.node.sub(keyfile)
+ os.chmod(keyfile_path, 0)
+ context.expect_msg("Error reading", code=0 if run_legacy_checks else 1)
+
+ if run_permission_checks:
+ context = test(f"warn about unsafe permissions on {keyfile}")
+ keyfile_path = context.node.sub(keyfile)
+ os.chmod(keyfile_path, 0o666)
+ context.expect_msg("unsafe file permissions", code=0)
+
+ if run_legacy_checks:
+ context = test(f"pass on missing {keyfile} when the other key is present")
+ keyfile_path = context.node.sub(keyfile)
+ os.remove(keyfile_path)
+ context.node.cmd("fsck")
+
+
+def test_ec_public_key_file_var(context: Context, *paths: str) -> None:
+ """Test EC public keys in config *paths."""
+ ec_pubkey = os.path.realpath(context.node.sub("ec_pubkey"))
+
+ ec_key = ""
+ for line in read_lines(context.host):
+ if line.startswith("Ed25519PublicKey"):
+ _, _, ec_key = line.split()
+ break
+ assert ec_key
+
+ pem = f"""
+-----BEGIN ED25519 PUBLIC KEY-----
+{ec_key}
+-----END ED25519 PUBLIC KEY-----
+"""
+ write_text(ec_pubkey, pem)
+
+ replace_line(context.host, "Ed25519PublicKey")
+
+ config = context.node.sub(*paths)
+ append_line(config, f"Ed25519PublicKeyFile = {ec_pubkey}")
+
+ context.expect_msg("No (usable) public Ed25519", code=0, present=False)
+
+
+###############################################################################
+# Common tests
+###############################################################################
+
+ctx = test("pass freshly created configuration")
+ctx.node.cmd("fsck")
+
+ctx = test("fail on missing tinc.conf")
+os.remove(ctx.conf)
+ctx.expect_msg("No tinc configuration found")
+
+for suffix in "up", "down":
+ ctx = test(f"unknown -{suffix} script warning")
+ fake_path = ctx.node.sub(f"fake-{suffix}")
+ write_text(fake_path, "")
+ ctx.expect_msg("Unknown script", code=0)
+
+ctx = test("fix broken Ed25519 public key with --force")
+replace_line(ctx.host, "Ed25519PublicKey", "Ed25519PublicKey = foobar")
+ctx.expect_msg("No (usable) public Ed25519 key", force=True, code=0)
+ctx.node.cmd("fsck")
+
+ctx = test("fix missing Ed25519 public key with --force")
+replace_line(ctx.host, "Ed25519PublicKey")
+ctx.expect_msg("No (usable) public Ed25519 key", force=True, code=0)
+ctx.node.cmd("fsck")
+
+ctx = test("fail when all private keys are missing")
+os.remove(ctx.ec_priv)
+if run_legacy_checks:
+ os.remove(ctx.rsa_priv)
+ ctx.expect_msg("Neither RSA or Ed25519 private")
+else:
+ ctx.expect_msg("No Ed25519 private")
+
+ctx = test("warn about missing EC public key and NOT fix without --force")
+replace_line(ctx.host, "Ed25519PublicKey")
+ctx.expect_msg("No (usable) public Ed25519", code=0)
+host = read_text(ctx.host)
+check.not_in("ED25519 PUBLIC KEY", host)
+
+ctx = test("fix missing EC public key on --force")
+replace_line(ctx.host, "Ed25519PublicKey")
+ctx.expect_msg("Wrote Ed25519 public key", force=True, code=0)
+host = read_text(ctx.host)
+check.is_in("ED25519 PUBLIC KEY", host)
+
+ctx = test("warn about obsolete variables")
+append_line(ctx.host, "GraphDumpFile = /dev/null")
+ctx.expect_msg("obsolete variable GraphDumpFile", code=0)
+
+ctx = test("warn about missing values")
+append_line(ctx.host, "Weight = ")
+ctx.expect_msg("No value for variable `Weight")
+
+ctx = test("warn about duplicate variables")
+append_line(ctx.host, f"Weight = 0{os.linesep}Weight = 1")
+ctx.expect_msg("multiple instances of variable Weight", code=0)
+
+ctx = test("warn about server variables in host config")
+append_line(ctx.host, "Interface = fake0")
+ctx.expect_msg("server variable Interface found", code=0)
+
+ctx = test("warn about host variables in server config")
+append_line(ctx.conf, "Port = 1337")
+ctx.expect_msg("host variable Port found", code=0)
+
+ctx = test("warn about missing Name")
+replace_line(ctx.conf, "Name =")
+ctx.expect_msg("without a valid Name")
+
+test_private_keys("ed25519_key.priv")
+test_private_key_var("Ed25519PrivateKeyFile", "ed25519_key.priv")
+
+ctx = test("test EC public key in tinc.conf")
+test_ec_public_key_file_var(ctx, "tinc.conf")
+
+ctx = test("test EC public key in hosts/")
+test_ec_public_key_file_var(ctx, "hosts", ctx.node.name)
+
+if run_access_checks:
+ ctx = test("fail on inaccessible tinc.conf")
+ os.chmod(ctx.conf, 0)
+ ctx.expect_msg("not running tinc as root")
+
+ ctx = test("fail on inaccessible hosts/foo")
+ os.chmod(ctx.host, 0)
+ ctx.expect_msg("Cannot open config file")
+
+if run_executability_checks:
+ ctx = test("non-executable tinc-up MUST be fixed by tinc --force")
+ os.chmod(ctx.tinc_up, 0o644)
+ ctx.expect_msg("cannot read and execute", force=True, code=0)
+ assert os.access(ctx.tinc_up, os.X_OK)
+
+ ctx = test("non-executable tinc-up MUST NOT be fixed by tinc without --force")
+ os.chmod(ctx.tinc_up, 0o644)
+ ctx.expect_msg("cannot read and execute", code=0)
+ assert not os.access(ctx.tinc_up, os.X_OK)
+
+ ctx = test("non-executable foo-up MUST be fixed by tinc --force")
+ write_text(ctx.host_up, "")
+ os.chmod(ctx.host_up, 0o644)
+ ctx.expect_msg("cannot read and execute", force=True, code=0)
+ assert os.access(ctx.tinc_up, os.X_OK)
+
+ ctx = test("non-executable bar-up MUST NOT be fixed by tinc")
+ path = ctx.node.sub("hosts", "bar-up")
+ write_text(path, "")
+ os.chmod(path, 0o644)
+ ctx.expect_msg("cannot read and execute", code=0)
+ assert not os.access(path, os.X_OK)
+
+###############################################################################
+# Legacy protocol
+###############################################################################
+if not run_legacy_checks:
+ log.info("skipping legacy protocol tests")
+ sys.exit(0)
+
+
+def test_rsa_public_key_file_var(context: Context, *paths: str) -> None:
+ """Test RSA public keys in config *paths."""
+ key = extract_pem(context.host)
+ remove_pem(context.host)
+
+ rsa_pub = os.path.realpath(context.node.sub("rsa_pubkey"))
+ write_lines(rsa_pub, key)
+
+ config = context.node.sub(*paths)
+ append_line(config, f"PublicKeyFile = {rsa_pub}")
+
+ context.expect_msg("Error reading RSA public key", code=0, present=False)
+
+
+test_private_keys("rsa_key.priv")
+test_private_key_var("PrivateKeyFile", "rsa_key.priv")
+
+ctx = test("test rsa public key in tinc.conf")
+test_rsa_public_key_file_var(ctx, "tinc.conf")
+
+ctx = test("test rsa public key in hosts/")
+test_rsa_public_key_file_var(ctx, "hosts", ctx.node.name)
+
+ctx = test("warn about missing RSA private key if public key is present")
+os.remove(ctx.rsa_priv)
+ctx.expect_msg("public RSA key was found but no private key", code=0)
+
+ctx = test("warn about missing RSA public key")
+remove_pem(ctx.host)
+ctx.expect_msg("No (usable) public RSA", code=0)
+check.not_in("BEGIN RSA PUBLIC KEY", read_text(ctx.host))
+
+ctx = test("fix missing RSA public key on --force")
+remove_pem(ctx.host)
+ctx.expect_msg("Wrote RSA public key", force=True, code=0)
+check.is_in("BEGIN RSA PUBLIC KEY", read_text(ctx.host))
+
+ctx = test("RSA PublicKey + PrivateKey must work")
+os.remove(ctx.rsa_priv)
+remove_pem(ctx.host)
+append_line(ctx.conf, f"PrivateKey = {RSA_D}")
+append_line(ctx.host, f"PublicKey = {RSA_N}")
+ctx.expect_msg("no (usable) public RSA", code=0, present=False)
+
+ctx = test("RSA PrivateKey without PublicKey must warn")
+os.remove(ctx.rsa_priv)
+remove_pem(ctx.host)
+append_line(ctx.conf, f"PrivateKey = {RSA_D}")
+ctx.expect_msg("PrivateKey used but no PublicKey found", code=0)
+
+ctx = test("warn about missing EC private key if public key is present")
+os.remove(ctx.ec_priv)
+ctx.expect_msg("public Ed25519 key was found but no private key", code=0)
+
+ctx = test("fix broken RSA public key with --force")
+host_lines = read_lines(ctx.host)
+del host_lines[1]
+write_lines(ctx.host, host_lines)
+ctx.expect_msg("old key(s) found and disabled", force=True, code=0)
+ctx.node.cmd("fsck")
+
+ctx = test("fix missing RSA public key with --force")
+remove_pem(ctx.host)
+ctx.expect_msg("No (usable) public RSA key found", force=True, code=0)
+ctx.node.cmd("fsck")
+
+if run_permission_checks:
+ ctx = test("warn about unsafe permissions on tinc.conf with PrivateKey")
+ os.remove(ctx.rsa_priv)
+ append_line(ctx.conf, f"PrivateKey = {RSA_D}")
+ append_line(ctx.host, f"PublicKey = {RSA_N}")
+ os.chmod(ctx.conf, 0o666)
+ ctx.expect_msg("unsafe file permissions", code=0)
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test supported and unsupported commandline flags."""
+
+from testlib import check, util
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+tinc_flags = (
+ (0, ("get", "name")),
+ (0, ("-n", "foo", "get", "name")),
+ (0, ("-nfoo", "get", "name")),
+ (0, ("--net=foo", "get", "name")),
+ (0, ("--net", "foo", "get", "name")),
+ (0, ("-c", "conf", "-c", "conf")),
+ (0, ("-n", "net", "-n", "net")),
+ (0, ("--pidfile=pid", "--pidfile=pid")),
+ (1, ("-n", "foo", "get", "somethingreallyunknown")),
+ (1, ("--net",)),
+ (1, ("--net", "get", "name")),
+ (1, ("foo",)),
+ (1, ("-c", "conf", "-n", "n/e\\t")),
+)
+
+tincd_flags = (
+ (0, ("-D",)),
+ (0, ("--no-detach",)),
+ (0, ("-D", "-d")),
+ (0, ("-D", "-d2")),
+ (0, ("-D", "-d", "2")),
+ (0, ("-D", "-n", "foo")),
+ (0, ("-D", "-nfoo")),
+ (0, ("-D", "--net=foo")),
+ (0, ("-D", "--net", "foo")),
+ (0, ("-D", "-c", ".", "-c", ".")),
+ (0, ("-D", "-n", "net", "-n", "net")),
+ (0, ("-D", "-n", "net", "-o", "FakeOpt=42")),
+ (0, ("-D", "--logfile=log", "--logfile=log")),
+ (0, ("-D", "--pidfile=pid", "--pidfile=pid")),
+ (1, ("foo",)),
+ (1, ("--pidfile",)),
+ (1, ("--foo",)),
+ (1, ("-n", "net", "-o", "Compression=")),
+ (1, ("-c", "fakedir", "-n", "n/e\\t")),
+)
+
+
+def init(ctx: Test) -> Tinc:
+ """Initialize new test nodes."""
+ tinc = ctx.node()
+ stdin = f"""
+ init {tinc}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ """
+ tinc.cmd(stdin=stdin)
+ tinc.add_script(Script.TINC_UP)
+ return tinc
+
+
+with Test("commandline flags") as context:
+ node = init(context)
+
+ for code, flags in tincd_flags:
+ COOKIE = util.random_string(10)
+ server = node.tincd(*flags, env={"COOKIE": COOKIE})
+
+ if not code:
+ log.info("waiting for tincd to come up")
+ env = node[Script.TINC_UP].wait().env
+ check.equals(COOKIE, env["COOKIE"])
+
+ log.info("stopping tinc")
+ node.cmd("stop", code=code)
+
+ log.info("reading tincd output")
+ stdout, stderr = server.communicate()
+
+ log.debug('got code %d, ("%s", "%s")', server.returncode, stdout, stderr)
+ check.equals(code, server.returncode)
+
+ for code, flags in tinc_flags:
+ node.cmd(*flags, code=code)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize one node
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port 0
-EOF
-
-create_script foo tinc-up '
- tinc foo stop &
-'
-
-echo [STEP] Test tincd command line options that should work
-
-tincd foo -D
-tincd foo --no-detach
-tincd foo -D -d
-tincd foo -D -d2
-tincd foo -D -d 2
-tincd foo -D -n foo
-tincd foo -D -nfoo
-tincd foo -D --net=foo
-tincd foo -D --net foo
-
-echo [STEP] Test tincd command line options that should not work
-
-expect_code "$EXIT_FAILURE" tincd foo foo
-expect_code "$EXIT_FAILURE" tincd foo --pidfile
-expect_code "$EXIT_FAILURE" tincd foo --foo
-
-echo [STEP] Test tinc command line options that should work
-
-tinc foo get name
-tinc foo -n foo get name
-tinc foo -nfoo get name
-tinc foo --net=foo get name
-tinc foo --net foo get name
-
-echo [STEP] Test tinc command line options that should not work
-
-expect_code "$EXIT_FAILURE" tinc foo -n foo get somethingreallyunknown
-expect_code "$EXIT_FAILURE" tinc foo --net
-expect_code "$EXIT_FAILURE" tinc foo --net get name
-expect_code "$EXIT_FAILURE" tinc foo foo
-
-# Most of these should fail with ASAN. Some leaks are only detected by Valgrind.
-echo [STEP] Trigger previously known memory leaks
-
-tincd foo -c . -c . --help
-tincd foo -n net -n net --help
-tincd foo -n net -o FakeOpt=42 --help
-tincd foo --logfile=one --logfile=two --help
-tincd foo --pidfile=one --pidfile=two --help
-expect_code "$EXIT_FAILURE" tincd foo -n net -o Compression= --help
-expect_code "$EXIT_FAILURE" tincd foo -c fakedir -n 'n/e\t'
-
-tinc foo -c conf -c conf --help
-tinc foo -n net -n net --help
-tinc foo --pidfile=pid --pidfile=pid --help
-expect_code "$EXIT_FAILURE" tinc foo -c conf -n 'n/e\t'
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test supported and unsupported compression levels."""
+
+import os
+import signal
+import sys
+import multiprocessing.connection as mpc
+import subprocess as subp
+import time
+import typing as T
+
+from testlib import external as ext, cmd, path, check, util
+from testlib.log import log
+from testlib.proc import Script, Tinc, Feature
+from testlib.test import Test
+from testlib.template import make_netns_config
+
+IP_FOO = "192.168.1.1"
+IP_BAR = "192.168.1.2"
+MASK = 24
+
+CONTENT = "zHgfHEzRsKPU41rWoTzmcxxxUGvjfOtTZ0ZT2S1GezL7QbAcMGiLa8i6JOgn59Dq5BtlfbZj"
+
+
+def run_receiver() -> None:
+ """Run server that receives data it prints it to stdout."""
+ with mpc.Listener((IP_FOO, 0), family="AF_INET") as listener:
+ port = listener.address[1]
+ sys.stdout.write(f"{port}\n")
+ sys.stdout.flush()
+
+ with listener.accept() as conn:
+ data = conn.recv()
+ print(data, sep="", flush=True)
+
+
+def run_sender() -> None:
+ """Start client that reads data from stdin and sends it to server."""
+ port = int(os.environ["PORT"])
+
+ for _ in range(5):
+ try:
+ with mpc.Client((IP_FOO, port)) as client:
+ client.send(CONTENT)
+ return
+ except OSError as ex:
+ log.warning("could not connect to receiver", exc_info=ex)
+ time.sleep(1)
+
+ log.error("failed to send data, terminating")
+ os.kill(0, signal.SIGTERM)
+
+
+def get_levels(features: T.Container[Feature]) -> T.Tuple[T.List[int], T.List[int]]:
+ """Get supported compression levels."""
+ log.info("getting supported compression levels")
+
+ levels: T.List[int] = []
+ bogus: T.List[int] = []
+
+ for comp, lvl_min, lvl_max in (
+ (Feature.COMP_ZLIB, 1, 9),
+ (Feature.COMP_LZO, 10, 11),
+ (Feature.COMP_LZ4, 12, 12),
+ ):
+ lvls = range(lvl_min, lvl_max + 1)
+ if comp in features:
+ levels += lvls
+ else:
+ bogus += lvls
+
+ log.info("supported compression levels: %s", levels)
+ log.info("unsupported compression levels: %s", bogus)
+
+ return levels, bogus
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar = ctx.node(addr=IP_FOO), ctx.node(addr=IP_BAR)
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set Address {foo.address}
+ set Subnet {foo.address}
+ set Interface {foo}
+ set Address localhost
+ """
+ foo.cmd(stdin=stdin)
+ assert ext.netns_add(foo.name)
+ foo.add_script(Script.TINC_UP, make_netns_config(foo.name, foo.address, MASK))
+
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set Address {bar.address}
+ set Subnet {bar.address}
+ set Interface {bar}
+ set ConnectTo {foo}
+ """
+ bar.cmd(stdin=stdin)
+ assert ext.netns_add(bar.name)
+ bar.add_script(Script.TINC_UP, make_netns_config(bar.name, bar.address, MASK))
+ foo.add_script(Script.SUBNET_UP)
+
+ log.info("start %s and exchange configuration", foo)
+ foo.start()
+ cmd.exchange(foo, bar)
+
+ return foo, bar
+
+
+def test_valid_level(foo: Tinc, bar: Tinc) -> None:
+ """Test that supported compression level works correctly."""
+ while True:
+ env = foo[Script.SUBNET_UP].wait().env
+ if env.get("SUBNET") == bar.address:
+ break
+
+ log.info("start receiver in netns")
+ with subp.Popen(
+ ["ip", "netns", "exec", foo.name, path.PYTHON_PATH, __file__, "--recv"],
+ stdout=subp.PIPE,
+ encoding="utf-8",
+ ) as receiver:
+ assert receiver.stdout
+ port = receiver.stdout.readline().strip()
+
+ log.info("start sender in netns")
+ with subp.Popen(
+ ["ip", "netns", "exec", bar.name, path.PYTHON_PATH, __file__, "--send"],
+ env={**dict(os.environ), "PORT": port},
+ ):
+ recv = receiver.stdout.read()
+ log.info('received %d bytes: "%s"', len(recv), recv)
+
+ check.equals(0, receiver.wait())
+ check.equals(CONTENT, recv.rstrip())
+
+
+def test_bogus_level(node: Tinc) -> None:
+ """Test that unsupported compression level fails to start."""
+ tincd = node.tincd()
+ _, stderr = tincd.communicate()
+ check.equals(1, tincd.returncode)
+ check.is_in("Bogus compression level", stderr)
+
+
+def run_tests() -> None:
+ """Run all tests."""
+ with Test("get supported levels") as ctx:
+ node = ctx.node()
+ levels, bogus = get_levels(node.features)
+
+ with Test("valid levels") as ctx:
+ foo, bar = init(ctx)
+ for level in levels:
+ for node in foo, bar:
+ node.cmd("set", "Compression", str(level))
+ bar.cmd("start")
+ test_valid_level(foo, bar)
+ bar.cmd("stop")
+
+ with Test("test bogus levels") as ctx:
+ node = ctx.node()
+ for level in bogus:
+ node.cmd("set", "Compression", str(level))
+ test_bogus_level(node)
+
+
+last = sys.argv[-1]
+
+if last == "--recv":
+ run_receiver()
+elif last == "--send":
+ run_sender()
+else:
+ util.require_root()
+ util.require_command("ip", "netns", "list")
+ util.require_path("/dev/net/tun")
+ run_tests()
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-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"
-
-ip_foo=192.168.1.1
-ip_bar=192.168.1.2
-port_foo=30100
-recv_port_foo=30101
-mask=24
-
-echo '[STEP] Determining supported compression levels'
-
-features=$(tincd foo --version)
-bogus_levels="-1 13"
-levels=0
-
-add_levels() {
- algo=$1
- shift
-
- if echo "$features" | grep "comp_$algo"; then
- levels="$levels $*"
- else
- bogus_levels="$bogus_levels $*"
- fi
-}
-
-add_levels zlib 1 2 3 4 5 6 7 8 9
-add_levels lzo 10 11
-add_levels lz4 12
-
-echo "Supported compression levels: $levels"
-echo "Unsupported compression levels: $bogus_levels"
-
-echo [STEP] Create network namespaces
-
-ip netns add foo
-ip netns add bar
-tmp_file=$(mktemp)
-
-cleanup_hook() {
- ip netns del foo
- ip netns del bar
- rm -f "$tmp_file"
-}
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set Subnet $ip_foo
-set Interface foo
-set Port $port_foo
-set Address localhost
-EOF
-
-tinc bar <<EOF
-init bar
-set Subnet $ip_bar
-set Interface bar
-set ConnectTo foo
-EOF
-
-# shellcheck disable=SC2016
-create_script foo tinc-up "
- ip link set dev \$INTERFACE netns foo
- ip netns exec foo ip addr add $ip_foo/$mask dev \$INTERFACE
- ip netns exec foo ip link set \$INTERFACE up
-"
-
-# shellcheck disable=SC2016
-create_script bar tinc-up "
- ip link set dev \$INTERFACE netns bar
- ip netns exec bar ip addr add $ip_bar/$mask dev \$INTERFACE
- ip netns exec bar ip link set \$INTERFACE up
-"
-
-echo [STEP] Exchange configuration files
-
-tinc foo export | tinc bar exchange | tinc foo import
-
-echo [STEP] Test supported compression levels
-
-ref_file=$0
-
-create_script foo hosts/bar-up
-create_script bar hosts/foo-up
-
-for level in $levels; do
- echo "[STEP] Testing compression level $level"
-
- tinc foo set Compression "$level"
- tinc bar set Compression "$level"
-
- start_tinc foo
- wait_script foo tinc-up
-
- start_tinc bar
- wait_script bar tinc-up
-
- wait_script foo hosts/bar-up
- wait_script bar hosts/foo-up
-
- sh <<EOF
- set -eu
- ip netns exec foo 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 &
- wait
-EOF
-
- diff -w "$ref_file" "$tmp_file"
-
- tinc foo stop
- tinc bar stop
-done
-
-echo '[STEP] Invalid compression levels should fail'
-
-for level in $bogus_levels; do
- echo "[STEP] Testing bogus compression level $level"
- tinc foo set Compression "$level"
-
- output=$(expect_code "$EXIT_FAILURE" start_tinc foo 2>&1)
-
- if ! echo "$output" | grep -q 'Bogus compression level'; then
- bail 'expected message about the wrong compression level'
- fi
-done
--- /dev/null
+#!/usr/bin/env python3
+
+"""Basic sanity checks on compiled executables."""
+
+from subprocess import run, PIPE
+
+from testlib import path, check
+from testlib.log import log
+
+for exe in (
+ path.TINC_PATH,
+ path.TINCD_PATH,
+ path.SPTPS_TEST_PATH,
+ path.SPTPS_KEYPAIR_PATH,
+):
+ cmd = [exe, "--help"]
+ log.info('testing command "%s"', cmd)
+ res = run(cmd, stdout=PIPE, stderr=PIPE, encoding="utf-8", timeout=10, check=False)
+ check.equals(0, res.returncode)
+ check.is_in("Usage:", res.stdout, res.stderr)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Just test whether the executables work
-
-tinc foo --help
-
-tincd foo --help
-
-if [ -e "$SPTPS_TEST_PATH" ]; then
- "$SPTPS_TEST_PATH" --help
-fi
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize three nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port 30000
-set Address localhost
-EOF
-
-tinc bar <<EOF
-init bar
-set DeviceType dummy
-set Port 0
-EOF
-
-tinc baz <<EOF
-init baz
-set DeviceType dummy
-set Port 0
-EOF
-
-echo [STEP] Test import, export and exchange commands
-
-tinc foo export | tinc bar exchange | tinc foo import
-
-echo [STEP] Test export-all and exchange-all
-
-tinc foo export-all | tinc baz exchange | tinc foo import
-tinc foo exchange-all </dev/null | tinc bar import
-
-echo [STEP] Test equivalence of host config files
-
-diff -w "$DIR_FOO/hosts/foo" "$DIR_BAR/hosts/foo"
-diff -w "$DIR_FOO/hosts/foo" "$DIR_BAZ/hosts/foo"
-diff -w "$DIR_FOO/hosts/bar" "$DIR_BAR/hosts/bar"
-diff -w "$DIR_FOO/hosts/bar" "$DIR_BAZ/hosts/bar"
-diff -w "$DIR_FOO/hosts/baz" "$DIR_BAR/hosts/baz"
-diff -w "$DIR_FOO/hosts/baz" "$DIR_BAZ/hosts/baz"
-
-echo [STEP] Check whether the nodes can connect to each other
-
-create_script foo tinc-up '
- tinc bar add ConnectTo foo
- tinc baz add ConnectTo foo
-'
-
-create_script foo hosts/bar-up
-create_script foo hosts/baz-up
-
-start_tinc foo
-
-wait_script foo tinc-up
-
-start_tinc bar
-start_tinc baz
-
-wait_script foo hosts/bar-up
-wait_script foo hosts/baz-up
-
-require_nodes foo 3
-require_nodes bar 3
-require_nodes baz 3
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test peer information import and export."""
+
+import typing as T
+
+from testlib import check, cmd
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar, baz = ctx.node(), ctx.node(), ctx.node()
+
+ log.info("configure %s", foo.name)
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ """
+ foo.cmd(stdin=stdin)
+
+ log.info("configure %s", bar.name)
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ """
+ bar.cmd(stdin=stdin)
+
+ log.info("configure %s", baz.name)
+ stdin = f"""
+ init {baz}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ """
+ baz.cmd(stdin=stdin)
+
+ return foo, bar, baz
+
+
+def run_tests(ctx: Test) -> None:
+ """Run all tests."""
+ foo, bar, baz = init(ctx)
+
+ tinc_up = f"""
+ bar, baz = Tinc('{bar}'), Tinc('{baz}')
+ bar.cmd('add', 'ConnectTo', this.name)
+ baz.cmd('add', 'ConnectTo', this.name)
+ """
+ foo.add_script(Script.TINC_UP, tinc_up)
+ foo.start()
+
+ log.info("run exchange")
+ cmd.exchange(foo, bar)
+
+ log.info("run exchange with export-all")
+ cmd.exchange(foo, baz, export_all=True)
+
+ log.info("run exchange-all")
+ out, err = foo.cmd("exchange-all", code=1)
+ check.is_in("No host configuration files imported", err)
+
+ log.info("run import")
+ bar.cmd("import", stdin=out)
+
+ for first, second in (
+ (foo.sub("hosts", foo.name), bar.sub("hosts", foo.name)),
+ (foo.sub("hosts", foo.name), baz.sub("hosts", foo.name)),
+ (foo.sub("hosts", bar.name), bar.sub("hosts", bar.name)),
+ (foo.sub("hosts", bar.name), baz.sub("hosts", bar.name)),
+ (foo.sub("hosts", baz.name), bar.sub("hosts", baz.name)),
+ (foo.sub("hosts", baz.name), baz.sub("hosts", baz.name)),
+ ):
+ log.info("comparing configs %s and %s", first, second)
+ check.files_eq(first, second)
+
+ log.info("create %s scripts", foo)
+ foo.add_script(bar.script_up)
+ foo.add_script(baz.script_up)
+
+ log.info("start nodes")
+ bar.cmd("start")
+ baz.cmd("start")
+
+ log.info("wait for up scripts")
+ foo[bar.script_up].wait()
+ foo[baz.script_up].wait()
+
+ for tinc in foo, bar, baz:
+ check.nodes(tinc, 3)
+
+
+with Test("import-export") as context:
+ run_tests(context)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize one node
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Mode switch
-set Broadcast no
-set Address localhost
-set Port 30010
-EOF
-
-start_tinc foo
-
-echo [STEP] Generate an invitation and let another node join the VPN
-
-invitation=$(tinc foo invite bar)
-tinc bar join "$invitation"
-
-echo [STEP] Test equivalence of host config files
-
-diff -w "$DIR_FOO/hosts/foo" "$DIR_BAR/hosts/foo"
-test "$(grep ^Ed25519PublicKey "$DIR_FOO/hosts/bar")" = "$(grep ^Ed25519PublicKey "$DIR_BAR/hosts/bar")"
-
-echo [STEP] Test Mode, Broadcast and ConnectTo statements
-
-test "$(tinc bar get Mode)" = switch
-test "$(tinc bar get Broadcast)" = no
-test "$(tinc bar get ConnectTo)" = foo
-
-echo [STEP] Check whether the new node can join the VPN
-
-tinc bar <<EOF
-set DeviceType dummy
-set Port 0
-EOF
-
-create_script foo hosts/bar-up
-create_script bar hosts/foo-up
-
-start_tinc bar
-
-wait_script foo hosts/bar-up
-wait_script bar hosts/foo-up
-
-require_nodes foo 2
-require_nodes bar 2
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize one node
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Mode switch
-set Broadcast no
-set Address localhost
-set Port 30020
-EOF
-
-echo [STEP] Generate an invitation offline and let another node join the VPN
-
-invitation=$(tinc foo invite bar)
-
-start_tinc foo
-tinc bar join "$invitation"
-
-echo [STEP] Test equivalence of host config files
-
-diff -w "$DIR_FOO/hosts/foo" "$DIR_BAR/hosts/foo"
-test "$(grep ^Ed25519PublicKey "$DIR_FOO/hosts/bar")" = "$(grep ^Ed25519PublicKey "$DIR_BAR/hosts/bar")"
-
-echo [STEP] Test Mode, Broadcast and ConnectTo statements
-
-test "$(tinc bar get Mode)" = switch
-test "$(tinc bar get Broadcast)" = no
-test "$(tinc bar get ConnectTo)" = foo
-
-echo [STEP] Check whether the new node can join the VPN
-
-tinc bar <<EOF
-set DeviceType dummy
-set Port 0
-EOF
-
-create_script foo hosts/bar-up
-create_script bar hosts/foo-up
-
-start_tinc bar
-
-wait_script foo hosts/bar-up
-wait_script bar hosts/foo-up
-
-require_nodes foo 2
-require_nodes bar 2
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize one node
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Address localhost
-set Port 30030
-EOF
-
-create_script foo tinc-up
-start_tinc foo
-wait_script foo tinc-up
-
-echo [STEP] Generate an invitation and let another node join the VPN
-
-# shellcheck disable=SC2016
-create_script foo invitation-created '
-cat >"$INVITATION_FILE" <<INVITE
-Name = $NODE
-Ifconfig = 93.184.216.34/24
-Route = 2606:2800:220:1::/64 2606:2800:220:1:248:1893:25c8:1946
-Route = 1.2.3.4 1234::
-
-$(tinc foo export)
-INVITE
-'
-
-tinc foo invite bar | tail -1 | tinc bar --batch join
-
-echo [STEP] Test equivalence of host config files
-
-diff -w "$DIR_FOO/hosts/foo" "$DIR_BAR/hosts/foo"
-test "$(grep ^Ed25519PublicKey "$DIR_FOO/hosts/bar")" = "$(grep ^Ed25519PublicKey "$DIR_BAR/hosts/bar")"
-
-echo [STEP] Check if the tinc-up.invitation file is created and contains the right commands
-
-bar_tinc_up="$DIR_BAR/tinc-up.invitation"
-test -f "$bar_tinc_up"
-
-grep -F -q "93.184.216.34/24" "$bar_tinc_up"
-grep -F -q "2606:2800:220:1::/64" "$bar_tinc_up"
-grep -F -q "2606:2800:220:1:248:1893:25c8:1946" "$bar_tinc_up"
-must_fail grep -F -q "1234::" "$bar_tinc_up"
-
-echo [STEP] Check that no tinc-up is created and that tinc-up.invitation is not executable
-
-must_fail test -x "$bar_tinc_up"
-must_fail test -f "$DIR_BAR/tinc-up"
--- /dev/null
+#!/usr/bin/env python3
+# pylint: disable=import-outside-toplevel
+
+"""Test tinc peer invitations."""
+
+from testlib import check, util
+from testlib.log import log
+from testlib.test import Test
+
+
+def run_invite_test(ctx: Test, start_before_invite: bool) -> None:
+ """Run tests. If start_before_invite is True,
+ tincd is started *before* creating invitation, and vice versa.
+ """
+ foo, bar = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ set Mode switch
+ set Broadcast no
+ """
+ foo.cmd(stdin=stdin)
+
+ if start_before_invite:
+ port = foo.start()
+
+ log.info("create invitation")
+ foo_invite, _ = foo.cmd("invite", bar.name)
+ assert foo_invite
+ foo_invite = foo_invite.strip()
+
+ if not start_before_invite:
+ port = foo.start()
+ foo_invite = foo_invite.replace(":0/", f":{port}/")
+
+ log.info("join second node with %s", foo_invite)
+ bar.cmd("join", foo_invite)
+ bar.cmd("set", "Port", "0")
+
+ if not start_before_invite:
+ log.info("%s thinks %s is using port 0, updating", bar, foo)
+ bar.cmd("set", f"{foo}.Port", str(port))
+
+ log.info("compare configs")
+ check.files_eq(foo.sub("hosts", foo.name), bar.sub("hosts", foo.name))
+
+ log.info("compare keys")
+
+ prefix = "Ed25519PublicKey"
+ foo_key = util.find_line(foo.sub("hosts", bar.name), prefix)
+ bar_key = util.find_line(bar.sub("hosts", bar.name), prefix)
+ check.equals(foo_key, bar_key)
+
+ log.info("checking Mode")
+ bar_mode, _ = bar.cmd("get", "Mode")
+ check.equals("switch", bar_mode.strip())
+
+ log.info("checking Broadcast")
+ bar_bcast, _ = bar.cmd("get", "Broadcast")
+ check.equals("no", bar_bcast.strip())
+
+ log.info("checking ConnectTo")
+ bar_conn, _ = bar.cmd("get", "ConnectTo")
+ check.equals(foo.name, bar_conn.strip())
+
+ log.info("configuring %s", bar.name)
+ bar.cmd("set", "DeviceType", "dummy")
+
+ log.info("adding scripts")
+ foo.add_script(bar.script_up)
+ bar.add_script(foo.script_up)
+
+ log.info("starting %s", bar.name)
+ bar.cmd("start")
+
+ log.info("waiting for nodes to come up")
+ foo[bar.script_up].wait()
+ bar[foo.script_up].wait()
+
+ log.info("checking required nodes")
+ check.nodes(foo, 2)
+ check.nodes(bar, 2)
+
+
+with Test("offline mode") as context:
+ run_invite_test(context, start_before_invite=False)
+
+with Test("online mode") as context:
+ run_invite_test(context, start_before_invite=True)
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test inviting tinc nodes through tinc-up script."""
+
+import os
+import typing as T
+
+from testlib import check, util
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+IFCONFIG = "93.184.216.34/24"
+ROUTES_IPV6 = ("2606:2800:220:1::/64", "2606:2800:220:1:248:1893:25c8:1946")
+BAD_IPV4 = "1234::"
+ED_PUBKEY = "Ed25519PublicKey"
+
+
+def make_inv_created(export_output: str) -> str:
+ """Generate script for invitation-created script."""
+ return f'''
+ node, invite = os.environ['NODE'], os.environ['INVITATION_FILE']
+ log.info('writing to invitation file %s, node %s', invite, node)
+
+ script = f"""
+Name = {{node}}
+Ifconfig = {IFCONFIG}
+Route = {' '.join(ROUTES_IPV6)}
+Route = 1.2.3.4 {BAD_IPV4}
+
+{export_output}
+""".strip()
+
+ with open(invite, 'w', encoding='utf-8') as f:
+ f.write(script)
+ '''
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ """
+ foo.cmd(stdin=stdin)
+ foo.start()
+
+ return foo, bar
+
+
+def run_tests(ctx: Test) -> None:
+ """Run all tests."""
+ foo, bar = init(ctx)
+
+ log.info("run export")
+ export, _ = foo.cmd("export")
+ assert export
+
+ log.info("adding invitation-created script")
+ code = make_inv_created(export)
+ foo.add_script(Script.INVITATION_CREATED, code)
+
+ log.info("inviting %s", bar)
+ url, _ = foo.cmd("invite", bar.name)
+ url = url.strip()
+ assert url
+
+ log.info('joining %s to %s with "%s"', bar, foo, url)
+ bar.cmd("--batch", "join", url)
+ bar.cmd("set", "Port", "0")
+
+ log.info("comparing host configs")
+ check.files_eq(foo.sub("hosts", foo.name), bar.sub("hosts", foo.name))
+
+ log.info("comparing public keys")
+ foo_key = util.find_line(foo.sub("hosts", bar.name), ED_PUBKEY)
+ bar_key = util.find_line(bar.sub("hosts", bar.name), ED_PUBKEY)
+ check.equals(foo_key, bar_key)
+
+ log.info("bar.tinc-up must not exist")
+ assert not os.path.exists(bar.sub("tinc-up"))
+
+ inv = bar.sub("tinc-up.invitation")
+ log.info("testing %s", inv)
+
+ content = util.read_text(inv)
+ check.is_in(IFCONFIG, content)
+ check.not_in(BAD_IPV4, content)
+
+ for route in ROUTES_IPV6:
+ check.is_in(route, content)
+
+ if os.name != "nt":
+ assert not os.access(inv, os.X_OK)
+
+
+with Test("invite-tinc-up") as context:
+ run_tests(context)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port 30060
-set Address localhost
-add Subnet 10.98.98.1
-set PingTimeout 2
-EOF
-
-tinc bar <<EOF
-init bar
-set DeviceType dummy
-set Port 0
-add Subnet 10.98.98.2
-set PingTimeout 2
-set MaxTimeout 2
-EOF
-
-echo [STEP] Exchange host config files
-
-tinc foo export | tinc bar exchange | tinc foo import
-tinc bar add ConnectTo foo
-
-echo [STEP] Foo 1.1, bar 1.0
-
-tinc bar set ExperimentalProtocol no
-tinc foo del bar.Ed25519PublicKey
-tinc bar del foo.Ed25519PublicKey
-
-create_script foo hosts/bar-up
-create_script bar hosts/foo-up
-
-start_tinc foo
-start_tinc bar
-
-wait_script foo hosts/bar-up
-wait_script bar hosts/foo-up
-
-require_nodes foo 2
-require_nodes bar 2
-
-tinc bar stop
-tinc foo stop
-
-test -z "$(tinc foo get bar.Ed25519PublicKey)"
-test -z "$(tinc bar get foo.Ed25519PublicKey)"
-
-echo [STEP] Foo 1.1, bar upgrades to 1.1
-
-tinc bar del ExperimentalProtocol
-
-start_tinc foo
-start_tinc bar
-
-wait_script foo hosts/bar-up
-wait_script bar hosts/foo-up
-
-require_nodes foo 2
-require_nodes bar 2
-
-tinc bar stop
-tinc foo stop
-
-test -n "$(tinc foo get bar.Ed25519PublicKey)"
-test -n "$(tinc bar get foo.Ed25519PublicKey)"
-
-echo [STEP] Bar downgrades, must no longer be allowed to connect
-
-tinc bar set ExperimentalProtocol no
-
-create_script foo subnet-up
-start_tinc foo
-wait_script foo subnet-up
-
-create_script bar subnet-up
-start_tinc bar
-wait_script bar subnet-up
-
-# There is no reliable way to wait for 'not connecting'.
-sleep 10
-
-require_nodes foo 1
-require_nodes bar 1
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test legacy protocol support (tinc 1.0)."""
+
+import typing as T
+
+from testlib import check, cmd
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+TIMEOUT = 2
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ add Subnet 10.98.98.1
+ set PingTimeout {TIMEOUT}
+ """
+ foo.cmd(stdin=stdin)
+ foo.start()
+
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ add Subnet 10.98.98.2
+ set PingTimeout {TIMEOUT}
+ set MaxTimeout {TIMEOUT}
+ """
+ bar.cmd(stdin=stdin)
+
+ cmd.exchange(foo, bar)
+ bar.cmd("add", "ConnectTo", foo.name)
+
+ foo.add_script(bar.script_up)
+ bar.add_script(foo.script_up)
+
+ return foo, bar
+
+
+def run_keys_test(foo: Tinc, bar: Tinc, empty: bool) -> None:
+ """Check that EC public keys match the expected values."""
+ bar.cmd("start")
+
+ foo[bar.script_up].wait()
+ bar[foo.script_up].wait()
+
+ check.nodes(foo, 2)
+ check.nodes(bar, 2)
+
+ foo_bar, _ = foo.cmd("get", f"{bar.name}.Ed25519PublicKey", code=None)
+ log.info('got key foo/bar "%s"', foo_bar)
+
+ bar_foo, _ = bar.cmd("get", f"{foo.name}.Ed25519PublicKey", code=None)
+ log.info('got key bar/foo "%s"', bar_foo)
+
+ assert not foo_bar == empty
+ assert not bar_foo == empty
+
+
+with Test("foo 1.1, bar 1.1") as context:
+ foo_node, bar_node = init(context)
+ run_keys_test(foo_node, bar_node, empty=False)
+
+with Test("foo 1.1, bar 1.0") as context:
+ foo_node, bar_node = init(context)
+ bar_node.cmd("set", "ExperimentalProtocol", "no")
+ foo_node.cmd("del", f"{bar_node}.Ed25519PublicKey")
+ bar_node.cmd("del", f"{foo_node}.Ed25519PublicKey")
+ run_keys_test(foo_node, bar_node, empty=True)
+
+with Test("bar 1.0 must not be allowed to connect") as context:
+ foo_node, bar_node = init(context)
+ bar_node.cmd("set", "ExperimentalProtocol", "no")
+
+ bar_up = bar_node.add_script(Script.SUBNET_UP)
+ bar_node.cmd("start")
+ bar_up.wait()
+
+ assert not foo_node[bar_node.script_up].wait(TIMEOUT * 2)
+ check.nodes(foo_node, 1)
+ check.nodes(bar_node, 1)
tests = [
- 'basic.test',
- 'commandline.test',
- 'executables.test',
- 'import-export.test',
- 'invite-join.test',
- 'invite-offline.test',
- 'invite-tinc-up.test',
- 'security.test',
- 'security-legacy.test',
- 'security-sptps.test',
- 'variables.test',
+ 'basic.py',
+ 'command_fsck.py',
+ 'commandline.py',
+ 'executables.py',
+ 'import_export.py',
+ 'invite_tinc_up.py',
+ 'invite.py',
+ 'scripts.py',
+ 'security.py',
+ 'splice.py',
+ 'sptps_basic.py',
+ 'variables.py',
]
if opt_crypto != 'nolegacy'
- tests += 'algorithms.test'
- tests += 'legacy-protocol.test'
-endif
-
-if os_name != 'windows'
- tests += 'sptps-basic.test'
+ tests += [
+ 'algorithms.py',
+ 'legacy_protocol.py',
+ ]
endif
if os_name == 'linux'
- tests += 'ns-ping.test'
-endif
-
-if os_name != 'sunos'
- tests += 'scripts.test'
+ tests += [
+ 'ns_ping.py',
+ 'compression.py',
+ ]
endif
exe_splice = executable(
build_by_default: false,
)
-env = environment()
-env.set('TINC_PATH', exe_tinc.full_path())
-env.set('TINCD_PATH', exe_tincd.full_path())
-env.set('SPTPS_TEST_PATH', exe_sptps_test.full_path())
-env.set('SPTPS_KEYPAIR_PATH', exe_sptps_keypair.full_path())
-env.set('SPLICE_PATH', exe_splice.full_path())
-env.set('TESTLIB_PATH', meson.current_source_dir() / 'testlib.sh')
+env_vars = {
+ 'TINC_PATH': exe_tinc.full_path(),
+ 'TINCD_PATH': exe_tincd.full_path(),
+ 'PYTHON_PATH': python_path,
+ 'SPLICE_PATH': exe_splice.full_path(),
+ 'SPTPS_TEST_PATH': exe_sptps_test.full_path(),
+ 'SPTPS_KEYPAIR_PATH': exe_sptps_keypair.full_path(),
+}
deps_test = [
exe_tinc,
]
test_wd = meson.current_build_dir()
+test_src = meson.current_source_dir()
foreach test_name : tests
- target = find_program(test_name, native: true)
+ if meson_version.version_compare('>=0.52')
+ env = environment(env_vars)
+ else
+ env = environment()
+ foreach k, v : env_vars
+ env.set(k, v)
+ endforeach
+ endif
+ env.set('TEST_NAME', test_name)
+
test(test_name,
- target,
+ python,
+ args: test_src / test_name,
suite: 'integration',
- timeout: 5 * 60,
+ timeout: 60,
env: env,
depends: deps_test,
workdir: test_wd)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-require_root "$0" "$@"
-test -e /dev/net/tun || exit "$EXIT_SKIP_TEST"
-ip netns list || exit "$EXIT_SKIP_TEST"
-
-ip_foo=192.168.1.1
-ip_bar=192.168.1.2
-mask=24
-
-echo [STEP] Create network namespaces
-
-ip netns add ping.test1
-ip netns add ping.test2
-
-cleanup_hook() {
- ip netns del ping.test1
- ip netns del ping.test2
-}
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set Subnet $ip_foo
-set Interface ping.test1
-set Port 30090
-set Address localhost
-set AutoConnect no
-EOF
-
-# shellcheck disable=SC2016
-create_script foo tinc-up "
- ip link set dev \$INTERFACE netns ping.test1
- ip netns exec ping.test1 ip addr add $ip_foo/$mask dev \$INTERFACE
- ip netns exec ping.test1 ip link set \$INTERFACE up
-"
-
-tinc bar <<EOF
-init bar
-set Subnet $ip_bar
-set Interface ping.test2
-set Port 30091
-set AutoConnect no
-EOF
-
-# shellcheck disable=SC2016
-create_script bar tinc-up "
- ip link set dev \$INTERFACE netns ping.test2
- ip netns exec ping.test2 ip addr add $ip_bar/$mask dev \$INTERFACE
- ip netns exec ping.test2 ip link set \$INTERFACE up
-"
-
-echo [STEP] Exchange configuration files
-
-tinc foo export | tinc bar exchange | tinc foo import
-
-echo [STEP] Start tinc
-
-start_tinc foo
-start_tinc bar
-
-wait_script foo tinc-up
-wait_script bar tinc-up
-
-echo [STEP] The nodes should not be able to ping each other if there is no connection
-
-must_fail ip netns exec ping.test1 ping -W1 -c3 $ip_bar
-
-echo [STEP] After connecting they should be
-
-create_script bar hosts/foo-up
-
-tinc bar add ConnectTo foo
-wait_script bar hosts/foo-up
-
-ip netns exec ping.test1 ping -W1 -c3 $ip_bar
--- /dev/null
+#!/usr/bin/env python3
+
+"""Create two network namespaces and run ping between them."""
+
+import subprocess as subp
+import typing as T
+
+from testlib import external as ext, util, template, cmd
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+util.require_root()
+util.require_command("ip", "netns", "list")
+util.require_path("/dev/net/tun")
+
+IP_FOO = "192.168.1.1"
+IP_BAR = "192.168.1.2"
+MASK = 24
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ foo, bar = ctx.node(), ctx.node()
+
+ log.info("create network namespaces")
+ assert ext.netns_add(foo.name)
+ assert ext.netns_add(bar.name)
+
+ log.info("initialize two nodes")
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set Subnet {IP_FOO}
+ set Interface {foo}
+ set Address localhost
+ set AutoConnect no
+ """
+ foo.cmd(stdin=stdin)
+ foo.add_script(Script.TINC_UP, template.make_netns_config(foo.name, IP_FOO, MASK))
+ foo.start()
+
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set Subnet {IP_BAR}
+ set Interface {bar}
+ set Address localhost
+ set AutoConnect no
+ """
+ bar.cmd(stdin=stdin)
+ bar.add_script(Script.TINC_UP, template.make_netns_config(bar.name, IP_BAR, MASK))
+
+ cmd.exchange(foo, bar)
+
+ return foo, bar
+
+
+def ping(namespace: str, ip_addr: str) -> int:
+ """Send pings between two network namespaces."""
+ log.info("pinging node from netns %s at %s", namespace, ip_addr)
+ proc = subp.run(
+ ["ip", "netns", "exec", namespace, "ping", "-W1", "-c1", ip_addr], check=False
+ )
+
+ log.info("ping finished with code %d", proc.returncode)
+ return proc.returncode
+
+
+with Test("ns-ping") as context:
+ foo_node, bar_node = init(context)
+ bar_node.cmd("start")
+
+ log.info("waiting for nodes to come up")
+ bar_node[Script.TINC_UP].wait()
+
+ log.info("ping must not work when there is no connection")
+ assert ping(foo_node.name, IP_BAR)
+
+ log.info("add script foo/host-up")
+ bar_node.add_script(foo_node.script_up)
+
+ log.info("add ConnectTo clause")
+ bar_node.cmd("add", "ConnectTo", foo_node.name)
+
+ log.info("bar waits for foo")
+ bar_node[foo_node.script_up].wait()
+
+ log.info("ping must work after connection is up")
+ assert not ping(foo_node.name, IP_BAR)
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test that all tincd scripts execute in correct order and contain expected env vars."""
+
+import os
+import typing as T
+
+from testlib import check
+from testlib.log import log
+from testlib.proc import Tinc, Script, ScriptType, TincScript
+from testlib.test import Test
+from testlib.util import random_string
+
+SUBNET_SERVER = ("10.0.0.1", "fec0::/64")
+SUBNET_CLIENT = ("10.0.0.2", "fec0::/64#5")
+NETNAMES = {
+ "server": "net_" + random_string(8),
+ "invite": "net_" + random_string(8),
+ "client": "net_" + random_string(8),
+}
+
+# Creation time for the last notification event we've received.
+# Used for checking that scripts are called in the correct order.
+# dict is to avoid angering linters by using `global` to update this value.
+last_time = {"time": -1}
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ server, client = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {server}
+ set Port 0
+ set DeviceType dummy
+ set Address 127.0.0.1
+ set AddressFamily ipv4
+ add Subnet {SUBNET_SERVER[0]}
+ add Subnet {SUBNET_SERVER[1]}
+ """
+ server.cmd(stdin=stdin)
+
+ for script in (
+ *Script,
+ server.script_up,
+ server.script_down,
+ client.script_up,
+ client.script_down,
+ ):
+ server.add_script(script)
+
+ return server, client
+
+
+def wait_script(script: TincScript) -> T.Dict[str, str]:
+ """Wait for script to finish and check that it was run by tincd *after* the
+ script that was used as the argument in the previous call to this function.
+
+ For example, to check that SUBNET_UP is called after TINC_UP:
+ wait_script(node[Script.TINC_UP])
+ wait_script(node[Script.SUBNET_UP])
+ """
+ msg = script.wait()
+ assert msg.created_at
+
+ log.debug(
+ "%s sent %d, prev %d, diff %d",
+ script,
+ msg.created_at,
+ last_time["time"],
+ msg.created_at - last_time["time"],
+ )
+
+ if msg.created_at <= last_time["time"]:
+ raise ValueError(f"script {script} started in wrong order")
+
+ last_time["time"] = msg.created_at
+ return msg.env
+
+
+def wait_tinc(server: Tinc, script: Script) -> None:
+ """Wait for TINC_UP / TINC_DOWN and check env vars."""
+ log.info("checking tinc: %s %s", server, script)
+
+ env = wait_script(server[script])
+ check.equals(NETNAMES["server"], env["NETNAME"])
+ check.equals(server.name, env["NAME"])
+ check.equals("dummy", env["DEVICE"])
+
+
+def wait_subnet(server: Tinc, script: Script, node: Tinc, subnet: str) -> None:
+ """Wait for SUBNET_UP / SUBNET_DOWN and check env vars."""
+ log.info("checking subnet: %s %s %s %s", server, script, node, subnet)
+
+ env = wait_script(server[script])
+ check.equals(NETNAMES["server"], env["NETNAME"])
+ check.equals(server.name, env["NAME"])
+ check.equals("dummy", env["DEVICE"])
+ check.equals(node.name, env["NODE"])
+
+ if node != server:
+ check.equals("127.0.0.1", env["REMOTEADDRESS"])
+ check.equals(str(node.port), env["REMOTEPORT"])
+
+ if "#" in subnet:
+ addr, weight = subnet.split("#")
+ check.equals(addr, env["SUBNET"])
+ check.equals(weight, env["WEIGHT"])
+ else:
+ check.equals(subnet, env["SUBNET"])
+
+
+def wait_host(server: Tinc, client: Tinc, script: ScriptType) -> None:
+ """Wait for HOST_UP / HOST_DOWN and check env vars."""
+ log.info("checking host: %s %s %s", server, client, script)
+
+ env = wait_script(server[script])
+ check.equals(NETNAMES["server"], env["NETNAME"])
+ check.equals(server.name, env["NAME"])
+ check.equals(client.name, env["NODE"])
+ check.equals("dummy", env["DEVICE"])
+ check.equals("127.0.0.1", env["REMOTEADDRESS"])
+ check.equals(str(client.port), env["REMOTEPORT"])
+
+
+def test_start_server(server: Tinc) -> None:
+ """Start server node and run checks on its scripts."""
+ server.cmd("-n", NETNAMES["server"], "start")
+ wait_tinc(server, Script.TINC_UP)
+
+ port = server.read_port()
+ server.cmd("set", "port", str(port))
+
+ log.info("test server subnet-up")
+ for sub in SUBNET_SERVER:
+ wait_subnet(server, Script.SUBNET_UP, server, sub)
+
+
+def test_invite_client(server: Tinc, client: Tinc) -> str:
+ """Check that client invitation scripts work."""
+ url, _ = server.cmd("-n", NETNAMES["invite"], "invite", client.name)
+ url = url.strip()
+ check.true(url)
+
+ env = wait_script(server[Script.INVITATION_CREATED])
+ check.equals(NETNAMES["invite"], env["NETNAME"])
+ check.equals(server.name, env["NAME"])
+ check.equals(client.name, env["NODE"])
+ check.equals(url, env["INVITATION_URL"])
+ assert os.path.isfile(env["INVITATION_FILE"])
+
+ return url
+
+
+def test_join_client(server: Tinc, client: Tinc, url: str) -> None:
+ """Test that client joining scripts work."""
+ client.cmd("-n", NETNAMES["client"], "join", url)
+
+ env = wait_script(server[Script.INVITATION_ACCEPTED])
+ check.equals(NETNAMES["server"], env["NETNAME"])
+ check.equals(server.name, env["NAME"])
+ check.equals(client.name, env["NODE"])
+ check.equals("dummy", env["DEVICE"])
+ check.equals("127.0.0.1", env["REMOTEADDRESS"])
+
+
+def test_start_client(server: Tinc, client: Tinc) -> None:
+ """Start client and check its script work."""
+ client.randomize_port()
+
+ stdin = f"""
+ set Address {client.address}
+ set ListenAddress {client.address}
+ set Port {client.port}
+ set DeviceType dummy
+ add Subnet {SUBNET_CLIENT[0]}
+ add Subnet {SUBNET_CLIENT[1]}
+ start
+ """
+ client.cmd(stdin=stdin)
+
+ log.info("test client scripts")
+ wait_host(server, client, Script.HOST_UP)
+ wait_host(server, client, client.script_up)
+
+ log.info("test client subnet-up")
+ for sub in SUBNET_CLIENT:
+ wait_subnet(server, Script.SUBNET_UP, client, sub)
+
+
+def test_stop_server(server: Tinc, client: Tinc) -> None:
+ """Stop server and check that its scripts work."""
+ server.cmd("stop")
+ wait_host(server, client, Script.HOST_DOWN)
+ wait_host(server, client, client.script_down)
+
+ log.info("test client subnet-down")
+ for sub in SUBNET_CLIENT:
+ wait_subnet(server, Script.SUBNET_DOWN, client, sub)
+
+ log.info("test server subnet-down")
+ for sub in SUBNET_SERVER:
+ wait_subnet(server, Script.SUBNET_DOWN, server, sub)
+
+ log.info("test tinc-down")
+ wait_tinc(server, Script.TINC_DOWN)
+
+
+def run_tests(ctx: Test) -> None:
+ """Run all tests."""
+ server, client = init(ctx)
+
+ log.info("start server")
+ test_start_server(server)
+
+ log.info("invite client")
+ url = test_invite_client(server, client)
+
+ log.info('join client via url "%s"', url)
+ test_join_client(server, client, url)
+
+ log.info("start client")
+ test_start_client(server, client)
+
+ log.info("stop server")
+ test_stop_server(server, client)
+
+
+with Test("scripts test") as context:
+ run_tests(context)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initializing server node
-
-port_foo=30040
-port_bar=30041
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port $port_foo
-set Address 127.0.0.1
-add Subnet 10.0.0.1
-add Subnet fec0::/64
-EOF
-
-echo [STEP] Setting up scripts
-
-OUT=$DIR_FOO/scripts.out
-rm -f "$OUT"
-
-for script in \
- tinc-up tinc-down \
- host-up host-down \
- subnet-up subnet-down \
- hosts/foo-up hosts/foo-down \
- hosts/bar-up hosts/bar-down \
- invitation-created invitation-accepted; do
-
- commands=$(
- cat <<EOF
- if is_windows && [ -n "\$INVITATION_FILE" ]; then
- INVITATION_FILE=\$(cygpath --unix -- "\$INVITATION_FILE")
- fi
- echo >>'$OUT' "$script" "$TINC_SCRIPT_VARS"
-EOF
- )
-
- create_script foo "$script" "$commands"
-done
-
-echo [STEP] Starting server node
-
-start_tinc foo -n netname
-wait_script foo subnet-up 2
-echo foo-started >>"$OUT"
-
-echo [STEP] Inviting client node
-
-url=$(tinc foo -n netname2 invite bar)
-file=$(basename "$(find "$DIR_FOO/invitations" -type f ! -name ed25519_key.priv)")
-
-if is_windows; then
- file=$(cygpath --unix -- "$file")
-fi
-
-wait_script foo invitation-created
-echo bar-invited >>"$OUT"
-
-echo [STEP] Joining client node
-
-tinc bar -n netname3 join "$url"
-wait_script foo invitation-accepted
-echo bar-joined >>"$OUT"
-
-echo [STEP] Starting client node
-
-tinc bar <<EOF
-set DeviceType dummy
-set Port $port_bar
-add Subnet 10.0.0.2
-add Subnet fec0::/64#5
-EOF
-
-start_tinc bar
-wait_script foo subnet-up 2
-echo bar-started-1 >>"$OUT"
-
-tinc foo debug 4
-tinc bar stop
-wait_script foo subnet-down 2
-echo bar-stopped >>"$OUT"
-
-tinc foo debug 5
-start_tinc bar
-wait_script foo subnet-up 2
-echo bar-started-2 >>"$OUT"
-
-echo [STEP] Stop server node
-
-tinc foo stop
-tinc bar stop
-wait_script foo tinc-down
-
-echo [STEP] Check if the script output is what is expected
-
-cat >"$OUT.expected" <<EOF
-tinc-up netname,foo,dummy,,,,,,,,,5
-subnet-up netname,foo,dummy,,foo,,,10.0.0.1,,,,5
-subnet-up netname,foo,dummy,,foo,,,fec0::/64,,,,5
-foo-started
-invitation-created netname2,foo,,,bar,,,,,$DIR_FOO/invitations/$file,$url,
-bar-invited
-invitation-accepted netname,foo,dummy,,bar,127.0.0.1,,,,,,5
-bar-joined
-host-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-hosts/bar-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-subnet-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,10.0.0.2,,,,5
-subnet-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,fec0::/64,5,,,5
-bar-started-1
-host-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,4
-hosts/bar-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,4
-subnet-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,10.0.0.2,,,,4
-subnet-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,fec0::/64,5,,,4
-bar-stopped
-host-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-hosts/bar-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-subnet-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,10.0.0.2,,,,5
-subnet-up netname,foo,dummy,,bar,127.0.0.1,$port_bar,fec0::/64,5,,,5
-bar-started-2
-host-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-hosts/bar-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,,,,,5
-subnet-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,10.0.0.2,,,,5
-subnet-down netname,foo,dummy,,bar,127.0.0.1,$port_bar,fec0::/64,5,,,5
-subnet-down netname,foo,dummy,,foo,,,10.0.0.1,,,,5
-subnet-down netname,foo,dummy,,foo,,,fec0::/64,,,,5
-tinc-down netname,foo,dummy,,,,,,,,,5
-EOF
-
-diff -w "$OUT" "$OUT.expected"
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Skip this test if tools are missing
-
-command -v nc >/dev/null || exit "$EXIT_SKIP_TEST"
-command -v timeout >/dev/null || exit "$EXIT_SKIP_TEST"
-
-foo_port=30110
-bar_port=30111
-
-# usage: splice protocol_version
-splice() {
- "$SPLICE_PATH" foo localhost $foo_port bar localhost $bar_port "$1" &
- sleep 10
-}
-
-# usage: send_with_timeout "data to send" "data expected to receive"
-send_with_timeout() {
- data=$1
- expected=$3
-
- result=$(
- (
- sleep 6
- printf "%s\n" "$data"
- ) | timeout 10 nc localhost $foo_port
- ) && exit 1
-
- test $? = "$EXIT_TIMEOUT"
-
- if [ -z "$expected" ]; then
- test -z "$result"
- else
- echo "$result" | grep -q "^$expected"
- fi
-}
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port $foo_port
-set Address localhost
-set PingTimeout 3
-set ExperimentalProtocol no
-set AutoConnect no
-set Subnet 10.96.96.1
-EOF
-
-tinc bar <<EOF
-init bar
-set DeviceType dummy
-set Port $bar_port
-set PingTimeout 3
-set MaxTimeout 3
-set ExperimentalProtocol no
-set AutoConnect no
-set Subnet 10.96.96.2
-EOF
-
-echo [STEP] Exchange host config files
-
-tinc foo export | tinc bar exchange | tinc foo import
-
-create_script foo subnet-up
-start_tinc foo
-wait_script foo subnet-up
-
-create_script bar subnet-up
-start_tinc bar
-wait_script bar subnet-up
-
-echo [STEP] No splicing allowed, legacy protocol
-
-splice 17.7
-pid=$!
-
-require_nodes foo 1
-require_nodes bar 1
-
-kill $pid
-
-tinc bar stop
-tinc foo stop
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Skip this test if tools are missing
-
-command -v nc >/dev/null || exit "$EXIT_SKIP_TEST"
-command -v timeout >/dev/null || exit "$EXIT_SKIP_TEST"
-
-foo_port=30120
-bar_port=30121
-
-# usage: splice protocol_version
-splice() {
- "$SPLICE_PATH" foo localhost $foo_port bar localhost $bar_port "$1" &
- sleep 10
-}
-
-# usage: send_with_timeout "data to send" "data expected to receive"
-send_with_timeout() {
- data=$1
- expected=$3
-
- result=$(
- (
- sleep 6
- printf "%s\n" "$data"
- ) | timeout 10 nc localhost $foo_port
- ) && exit 1
-
- test $? = "$EXIT_TIMEOUT"
-
- if [ -z "$expected" ]; then
- test -z "$result"
- else
- echo "$result" | grep -q "^$expected"
- fi
-}
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port $foo_port
-set Address localhost
-set PingTimeout 3
-set AutoConnect no
-set Subnet 10.96.96.1
-EOF
-
-tinc bar <<EOF
-init bar
-set DeviceType dummy
-set Port $bar_port
-set PingTimeout 3
-set MaxTimeout 3
-set AutoConnect no
-set Subnet 10.96.96.2
-EOF
-
-echo [STEP] Exchange host config files
-
-tinc foo export | tinc bar exchange | tinc foo import
-
-create_script foo subnet-up
-start_tinc foo
-wait_script foo subnet-up
-
-create_script bar subnet-up
-start_tinc bar
-wait_script bar subnet-up
-
-echo [STEP] No splicing allowed, SPTPS
-
-splice 17.7
-pid=$!
-
-require_nodes foo 1
-require_nodes bar 1
-
-kill $pid
-
-tinc bar stop
-tinc foo stop
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test tinc protocol security."""
+
+import asyncio
+import typing as T
+
+from testlib import check
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+TIMEOUT = 2
+
+
+async def recv(read: asyncio.StreamReader, out: T.List[bytes]) -> None:
+ """Receive data until connection is closed."""
+ while not read.at_eof():
+ rec = await read.read(1)
+ out.append(rec)
+
+
+async def send(port: int, buf: str, delay: float = 0) -> bytes:
+ """Send data and receive response."""
+ raw = f"{buf}\n".encode("utf-8")
+ read, write = await asyncio.open_connection(host="localhost", port=port)
+
+ if delay:
+ await asyncio.sleep(delay)
+
+ received: T.List[bytes] = []
+ try:
+ write.write(raw)
+ await asyncio.wait_for(recv(read, received), timeout=1)
+ except asyncio.TimeoutError:
+ log.info('received: "%s"', received)
+ return b"".join(received)
+
+ raise RuntimeError("test should not have reached this line")
+
+
+async def test_id_timeout(foo: Tinc) -> None:
+ """Test that peer does not send its ID before us."""
+ log.info("no ID sent by peer if we don't send ID before the timeout")
+ data = await send(foo.port, "0 bar 17.7", delay=TIMEOUT * 1.5)
+ check.false(data)
+
+
+async def test_tarpitted(foo: Tinc) -> None:
+ """Test that peer sends its ID if we send first and are in tarpit."""
+ log.info("ID sent if initiator sends first, but still tarpitted")
+ data = await send(foo.port, "0 bar 17.7")
+ check.has_prefix(data, f"0 {foo} 17.7".encode("utf-8"))
+
+
+async def test_invalid_id_own(foo: Tinc) -> None:
+ """Test that peer does not accept its own ID."""
+ log.info("own ID not allowed")
+ data = await send(foo.port, f"0 {foo} 17.7")
+ check.false(data)
+
+
+async def test_invalid_id_unknown(foo: Tinc) -> None:
+ """Test that peer does not accept unknown ID."""
+ log.info("no unknown IDs allowed")
+ data = await send(foo.port, "0 baz 17.7")
+ check.false(data)
+
+
+async def test_null_metakey(foo: Tinc) -> None:
+ """Test that NULL metakey is not accepted."""
+ null_metakey = f"""
+0 {foo} 17.0\
+1 0 672 0 0 834188619F4D943FD0F4B1336F428BD4AC06171FEABA66BD2356BC9593F0ECD643F\
+0E4B748C670D7750DFDE75DC9F1D8F65AB1026F5ED2A176466FBA4167CC567A2085ABD070C1545B\
+180BDA86020E275EA9335F509C57786F4ED2378EFFF331869B856DDE1C05C461E4EECAF0E2FB97A\
+F77B7BC2AD1B34C12992E45F5D1254BBF0C3FB224ABB3E8859594A83B6CA393ED81ECAC9221CE6B\
+C71A727BCAD87DD80FC0834B87BADB5CB8FD3F08BEF90115A8DF1923D7CD9529729F27E1B8ABD83\
+C4CF8818AE10257162E0057A658E265610B71F9BA4B365A20C70578FAC65B51B91100392171BA12\
+A440A5E93C4AA62E0C9B6FC9B68F953514AAA7831B4B2C31C4
+""".strip()
+
+ log.info("no NULL METAKEY allowed")
+ data = await send(foo.port, null_metakey)
+ check.false(data)
+
+
+def init(ctx: Test) -> Tinc:
+ """Initialize new test nodes."""
+ foo = ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ set PingTimeout {TIMEOUT}
+ set AutoConnect no
+ set Subnet 10.96.96.1
+ """
+ foo.cmd(stdin=stdin)
+
+ foo.add_script(Script.SUBNET_UP)
+ foo.start()
+ foo[Script.SUBNET_UP].wait()
+
+ return foo
+
+
+async def run_tests(ctx: Test) -> None:
+ """Run all tests."""
+ foo = init(ctx)
+
+ log.info("getting into tarpit")
+ await test_id_timeout(foo)
+
+ log.info("starting other tests")
+ await asyncio.gather(
+ test_invalid_id_own(foo),
+ test_invalid_id_unknown(foo),
+ test_null_metakey(foo),
+ )
+
+
+loop = asyncio.get_event_loop()
+
+with Test("security") as context:
+ loop.run_until_complete(run_tests(context))
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Skip this test if tools are missing
-
-command -v nc >/dev/null || exit "$EXIT_SKIP_TEST"
-command -v timeout >/dev/null || exit "$EXIT_SKIP_TEST"
-
-foo_port=30050
-
-# usage: send_with_timeout "data to send" "data expected to receive"
-send_with_timeout() {
- data=$1
- expected=$3
-
- result=$(
- (
- sleep 6
- printf "%s\n" "$data"
- ) | timeout 10 nc localhost $foo_port
- ) && exit 1
-
- test $? = "$EXIT_TIMEOUT"
-
- if [ -z "$expected" ]; then
- test -z "$result"
- else
- echo "$result" | grep -q "^$expected"
- fi
-}
-
-echo [STEP] Initialize two nodes
-
-tinc foo <<EOF
-init foo
-set DeviceType dummy
-set Port $foo_port
-set Address localhost
-set PingTimeout 3
-set AutoConnect no
-set Subnet 10.96.96.1
-EOF
-
-create_script foo subnet-up
-start_tinc foo
-wait_script foo subnet-up
-
-echo "[STEP] No ID sent by responding node if we don't send an ID first, before the timeout"
-send_with_timeout "0 bar 17.7" "" &
-job1=$!
-
-echo [STEP] ID sent if initiator sends first, but still tarpitted
-send_with_timeout "0 bar 17.7" "0 foo 17.7" &
-job2=$!
-
-echo [STEP] No invalid IDs allowed
-send_with_timeout "0 foo 17.7" "" &
-job3=$!
-send_with_timeout "0 baz 17.7" "" &
-job4=$!
-
-echo [STEP] No NULL METAKEYs allowed
-data="0 foo 17.0\n1 0 672 0 0 834188619F4D943FD0F4B1336F428BD4AC06171FEABA66BD2356BC9593F0ECD643F0E4B748C670D7750DFDE75DC9F1D8F65AB1026F5ED2A176466FBA4167CC567A2085ABD070C1545B180BDA86020E275EA9335F509C57786F4ED2378EFFF331869B856DDE1C05C461E4EECAF0E2FB97AF77B7BC2AD1B34C12992E45F5D1254BBF0C3FB224ABB3E8859594A83B6CA393ED81ECAC9221CE6BC71A727BCAD87DD80FC0834B87BADB5CB8FD3F08BEF90115A8DF1923D7CD9529729F27E1B8ABD83C4CF8818AE10257162E0057A658E265610B71F9BA4B365A20C70578FAC65B51B91100392171BA12A440A5E93C4AA62E0C9B6FC9B68F953514AAA7831B4B2C31C4\n"
-send_with_timeout "$data" "" & # Not even the ID should be sent when the first packet contains illegal data
-job5=$!
-
-for pid in $job1 $job2 $job3 $job4 $job5; do
- wait $pid
-done
-
-tinc foo stop
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test splicing connection between tinc peers."""
+
+import os
+import subprocess as subp
+import typing as T
+
+from testlib import check, cmd, path
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+
+
+def init(ctx: Test, *options: str) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ custom = os.linesep.join(options)
+ log.info('init two nodes with options "%s"', custom)
+
+ foo, bar = ctx.node(), ctx.node()
+
+ stdin = f"""
+ init {foo}
+ set Port 0
+ set DeviceType dummy
+ set Address localhost
+ set AutoConnect no
+ set Subnet 10.96.96.1
+ {custom}
+ """
+ foo.cmd(stdin=stdin)
+
+ stdin = f"""
+ init {bar}
+ set Port 0
+ set Address localhost
+ set DeviceType dummy
+ set AutoConnect no
+ set Subnet 10.96.96.2
+ {custom}
+ """
+ bar.cmd(stdin=stdin)
+
+ foo.add_script(Script.SUBNET_UP)
+ bar.add_script(Script.SUBNET_UP)
+
+ foo.start()
+ bar.start()
+
+ log.info("exchange host configs")
+ cmd.exchange(foo, bar)
+
+ return foo, bar
+
+
+def splice(foo: Tinc, bar: Tinc, protocol: str) -> subp.Popen:
+ """Start splice between nodes."""
+ args = [
+ path.SPLICE_PATH,
+ foo.name,
+ "localhost",
+ str(foo.port),
+ bar.name,
+ "localhost",
+ str(bar.port),
+ protocol,
+ ]
+ log.info("splice with args %s", args)
+ return subp.Popen(args)
+
+
+def test_splice(ctx: Test, protocol: str, *options: str) -> None:
+ """Splice connection and check that it fails."""
+ log.info("no splicing allowed (%s)", protocol)
+ foo, bar = init(ctx, *options)
+
+ log.info("waiting for subnets to come up")
+ foo[Script.SUBNET_UP].wait()
+ bar[Script.SUBNET_UP].wait()
+
+ splice_proc = splice(foo, bar, protocol)
+ try:
+ check.nodes(foo, 1)
+ check.nodes(bar, 1)
+ finally:
+ splice_proc.kill()
+
+
+with Test("sptps") as context:
+ test_splice(context, "17.7")
+
+with Test("legacy") as context:
+ test_splice(context, "17.0", "set ExperimentalProtocol no")
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Skip this test if we did not compile sptps_test
-
-test -e "$SPTPS_TEST" -a -e "$SPTPS_KEYPAIR_PATH" || exit "$EXIT_SKIP_TEST"
-
-port=30080
-
-server_priv="$DIR_FOO/server.priv"
-client_priv="$DIR_FOO/client.priv"
-server_pub="$DIR_FOO/server.pub"
-client_pub="$DIR_FOO/client.pub"
-
-echo [STEP] Generate keys
-
-mkdir -p "$DIR_FOO"
-"$SPTPS_KEYPAIR_PATH" "$server_priv" "$server_pub"
-"$SPTPS_KEYPAIR_PATH" "$client_priv" "$client_pub"
-
-echo [STEP] Test transfer of a simple file
-
-reference=testlib.sh
-
-(
- sleep 3
- "$SPTPS_TEST_PATH" -4 -q "$client_priv" "$server_pub" localhost $port <"$reference"
-) &
-
-"$SPTPS_TEST_PATH" -4 "$server_priv" "$client_pub" $port >"$DIR_FOO/out1"
-diff -w "$DIR_FOO/out1" "$reference"
-
-"$SPTPS_TEST_PATH" -4 -q "$server_priv" "$client_pub" $port <"$reference" &
-sleep 3
-"$SPTPS_TEST_PATH" -4 "$client_priv" "$server_pub" localhost $port >"$DIR_FOO/out2"
-diff -w "$DIR_FOO/out2" "$reference"
-
-echo [STEP] Datagram mode
-
-"$SPTPS_TEST_PATH" -4 -dq "$server_priv" "$client_pub" $port <"$reference" &
-sleep 3
-sleep 3 | "$SPTPS_TEST_PATH" -4 -dq "$client_priv" "$server_pub" localhost $port >"$DIR_FOO/out3"
-diff -w "$DIR_FOO/out3" "$reference"
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test basic SPTPS features."""
+
+import os
+import subprocess as subp
+import re
+
+from testlib import path, util, check
+from testlib.log import log
+
+port_re = re.compile(r"Listening on (\d+)\.\.\.")
+
+
+class Keypair:
+ """Create public/private keypair using sptps_keypair."""
+
+ private: str
+ public: str
+
+ def __init__(self, name: str) -> None:
+ self.private = os.path.join(path.TEST_WD, f"{name}.priv")
+ self.public = os.path.join(path.TEST_WD, f"{name}.pub")
+ subp.run([path.SPTPS_KEYPAIR_PATH, self.private, self.public], check=True)
+
+
+log.info("generate keys")
+server_key = Keypair("server")
+client_key = Keypair("client")
+
+log.info("transfer random data")
+DATA = util.random_string(256).encode("utf-8")
+
+
+def run_client(port: int, key_priv: str, key_pub: str, *flags: str) -> None:
+ """Start client version of sptps_test."""
+ client_cmd = [
+ path.SPTPS_TEST_PATH,
+ "-4",
+ "-q",
+ *flags,
+ key_priv,
+ key_pub,
+ "localhost",
+ str(port),
+ ]
+ log.info('start client with "%s"', " ".join(client_cmd))
+ subp.run(client_cmd, input=DATA, check=True)
+
+
+def get_port(server: subp.Popen) -> int:
+ """Get port that sptps_test server is listening on."""
+ assert server.stderr
+ while True:
+ line = server.stderr.readline().decode("utf-8")
+ match = port_re.match(line)
+ if match:
+ return int(match[1])
+ log.debug("waiting for server to start accepting connections")
+
+
+def test(key0: Keypair, key1: Keypair, *flags: str) -> None:
+ """Run tests using the supplied keypair."""
+ server_cmd = [path.SPTPS_TEST_PATH, "-4", *flags, key0.private, key1.public, "0"]
+ log.info('start server with "%s"', " ".join(server_cmd))
+
+ with subp.Popen(server_cmd, stdout=subp.PIPE, stderr=subp.PIPE) as server:
+ assert server.stdout
+
+ port = get_port(server)
+ run_client(port, key1.private, key0.public, *flags)
+
+ received = b""
+ while len(received) < len(DATA):
+ received += server.stdout.read()
+
+ if server.returncode is None:
+ server.kill()
+
+ check.equals(DATA, received)
+
+
+def run_keypair_tests(*flags: str) -> None:
+ """Run tests on all generated keypairs."""
+ log.info("running tests with (client, server) keypair and flags %s", flags)
+ test(server_key, client_key)
+
+ log.info("running tests with (server, client) keypair and flags %s", flags)
+ test(client_key, server_key)
+
+
+log.info("running tests in stream mode")
+run_keypair_tests()
+
+log.info("running tests in datagram mode")
+run_keypair_tests("-dq")
+++ /dev/null
-#!/bin/sh
-
-set -ex
-
-echo [STEP] Initialize test library
-
-# Paths to compiled executables
-
-# realpath on FreeBSD fails if the path does not exist.
-realdir() {
- [ -e "$1" ] || mkdir -p "$1"
- if type realpath >/dev/null; then
- realpath "$1"
- else
- readlink -f "$1"
- fi
-}
-
-# Exit status list
-# shellcheck disable=SC2034
-EXIT_FAILURE=1
-# shellcheck disable=SC2034
-EXIT_SKIP_TEST=77
-
-# The list of the environment variables that tinc injects into the scripts it calls.
-# shellcheck disable=SC2016
-TINC_SCRIPT_VARS='$NETNAME,$NAME,$DEVICE,$IFACE,$NODE,$REMOTEADDRESS,$REMOTEPORT,$SUBNET,$WEIGHT,$INVITATION_FILE,$INVITATION_URL,$DEBUG'
-
-# Test directories
-
-# Reuse script name if it was passed in an env var (when imported from tinc scripts).
-if [ -z "$SCRIPTNAME" ]; then
- SCRIPTNAME=$(basename "$0")
-fi
-
-# Network names for tincd daemons.
-net1=$SCRIPTNAME.1
-net2=$SCRIPTNAME.2
-net3=$SCRIPTNAME.3
-
-# Configuration/pidfile directories for tincd daemons.
-DIR_FOO=$(realdir "$PWD/$net1")
-DIR_BAR=$(realdir "$PWD/$net2")
-DIR_BAZ=$(realdir "$PWD/$net3")
-
-# Register helper functions
-
-if [ "$(uname -s)" = SunOS ]; then
- gnu=/usr/gnu/bin
- grep="$gnu/grep"
-
- grep() { $gnu/grep "$@"; }
- tail() { $gnu/tail "$@"; }
-
- if ! tail /dev/null || ! echo '' | grep ''; then
- echo >&2 'Sorry, native Solaris tools are not supported. Please install GNU Coreutils.'
- exit $EXIT_SKIP_TEST
- fi
-else
- grep='grep'
-fi
-
-# Alias gtimeout to timeout if it exists.
-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
-}
-
-# busybox timeout returns 128 + signal number (which is TERM by default)
-if is_busybox; then
- # shellcheck disable=SC2034
- EXIT_TIMEOUT=$((128 + 15))
-else
- # shellcheck disable=SC2034
- EXIT_TIMEOUT=124
-fi
-
-# Is this msys2?
-is_windows() {
- test "$(uname -o)" = Msys
-}
-
-# Are we running on a CI server?
-is_ci() {
- test "$CI"
-}
-
-# Dump error message and exit with an error.
-bail() {
- echo >&2 "$@"
- exit 1
-}
-
-# Remove carriage returns to normalize strings on Windows for easier comparisons.
-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
- bail "expected a non-zero exit code"
- 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
- shift
-
- code=0
- "$@" || code=$?
-
- if [ $code != "$expected" ]; then
- bail "wrong exit code $code, expected $expected"
- fi
-}
-
-# wc -l on mac prints whitespace before the actual number.
-# This is simplest cross-platform alternative without that behavior.
-count_lines() {
- awk 'END{ print NR }'
-}
-
-# Calls compiled tinc, passing any supplied arguments.
-# Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
-tinc() {
- peer=$1
- shift
-
- case "$peer" in
- foo) "$TINC_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;;
- bar) "$TINC_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;;
- baz) "$TINC_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;;
- *) bail "invalid command [[$peer $*]]" ;;
- esac
-}
-
-# Calls compiled tincd, passing any supplied arguments.
-# Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
-tincd() {
- peer=$1
- shift
-
- case "$peer" in
- foo) "$TINCD_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;;
- bar) "$TINCD_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;;
- baz) "$TINCD_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;;
- *) bail "invalid command [[$peer $*]]" ;;
- esac
-}
-
-# Start the specified tinc daemon.
-# usage: start_tinc { foo | bar | baz }
-start_tinc() {
- peer=$1
- shift
-
- case "$peer" in
- foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;;
- bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;;
- baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;;
- *) bail "invalid peer $peer" ;;
- esac
-}
-
-# Stop all tinc clients.
-stop_all_tincs() {
- (
- # In case these pid files are mangled.
- set +e
- [ -f "$DIR_FOO/pid" ] && tinc foo stop
- [ -f "$DIR_BAR/pid" ] && tinc bar stop
- [ -f "$DIR_BAZ/pid" ] && tinc baz stop
- true
- )
-}
-
-# Checks that the number of reachable nodes matches what is expected.
-# usage: require_nodes node_name expected_number
-require_nodes() {
- echo >&2 "Check that we're able to reach tincd"
- test "$(tinc "$1" pid | count_lines)" = 1
-
- echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
- actual="$(tinc "$1" dump reachable nodes | count_lines)"
-
- if [ "$actual" != "$2" ]; then
- echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
- exit 1
- fi
-}
-
-peer_directory() {
- peer=$1
- case "$peer" in
- foo) echo "$DIR_FOO" ;;
- bar) echo "$DIR_BAR" ;;
- baz) echo "$DIR_BAZ" ;;
- *) bail "invalid peer $peer" ;;
- esac
-}
-
-# This is an append-only log of all scripts executed by all peers.
-script_runs_log() {
- echo "$(peer_directory "$1")/script-runs.log"
-}
-
-# Create tincd script. If it fails, it kills the test script with SIGTERM.
-# usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content'
-create_script() {
- peer=$1
- script=$2
- shift 2
-
- # This is the line that we should start from when reading the script execution log while waiting
- # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with
- # "last seen" files. There seem to be no good solutions to this that are compatible with all shells.
- line_var=$(next_line_var "$peer" "$script")
-
- # We must reassign it here in case the script is recreated.
- # shellcheck disable=SC2229
- read -r "$line_var" <<EOF
-1
-EOF
-
- # Full path to the script.
- script_path=$(peer_directory "$peer")/$script
-
- # Full path to the script execution log (one for each peer).
- script_log=$(script_runs_log "$peer")
- printf '' >"$script_log"
-
- # Script output is redirected into /dev/null. Otherwise, it ends up
- # in tinc's output and breaks things like 'tinc invite'.
- cat >"$script_path" <<EOF
-#!/bin/sh
-(
- cd "$PWD" || exit 1
- SCRIPTNAME="$SCRIPTNAME" . "$TESTLIB_PATH"
- $@
- echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
-) >/dev/null 2>&1 || kill -TERM $$
-EOF
-
- chmod u+x "$script_path"
-
- if is_windows; then
- echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
- fi
-}
-
-# Returns the name of the variable that contains the line number
-# we should read next when waiting on $script from $peer.
-# usage: next_line_var foo host-up
-next_line_var() {
- peer=$1
- script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
- printf "%s" "next_line_${peer}_${script}"
-}
-
-# Waits for `peer`'s script `script` to finish `count` number of times.
-# usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1]
-wait_script() {
- peer=$1
- script=$2
- count=$3
-
- if [ -z "$count" ] || [ "$count" -lt 1 ]; then
- count=1
- fi
-
- # Find out the location of the log and how many lines we should skip
- # (because we've already seen them in previous invocations of wait_script
- # for current $peer and $script).
- line_var=$(next_line_var "$peer" "$script")
-
- # eval is the only solution supported by POSIX shells.
- # https://github.com/koalaman/shellcheck/wiki/SC3053
- # 1. $line_var expands into 'next_line_foo_hosts_bar_up'
- # 2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"'
- # 3. the command is evaluated and the line number is assigned to $line
- line=$(eval "echo \"\$$line_var\"")
-
- # This is the file that we monitor for script execution records.
- script_log=$(script_runs_log "$peer")
-
- # Starting from $line, read until $count matches are found.
- # Print the number of the last matching line and exit.
- # GNU tail 2.82 and newer terminates by itself when the pipe breaks.
- # To support other tails we do an explicit `kill`.
- # FIFO is useful here because otherwise it's difficult to determine
- # which tail process should be killed. We could stick them in a process
- # group by enabling job control, but this results in weird behavior when
- # running tests in parallel on some interactive shells
- # (e.g. when /bin/sh is symlinked to dash).
- fifo=$(mktemp)
- rm -f "$fifo"
- mkfifo "$fifo"
-
- # This weird thing is required to support old versions of ksh on NetBSD 8.2 and the like.
- (tail -n +"$line" -f "$script_log" >"$fifo") &
-
- new_line=$(
- sh -c "
- $grep -n -m $count '^$script,' <'$fifo'
- " | awk -F: 'END { print $1 }'
- )
-
- # Try to stop the background tail, ignoring possible failure (some tails
- # detect EOF, some don't, so it may have already exited), but do wait on
- # it (which is required at least by old ksh).
- kill $! || true
- wait || true
- rm -f "$fifo"
-
- # Remember the next line number for future reference. We'll use it if
- # wait_script is called again with same $peer and $script.
- read -r "${line_var?}" <<EOF
-$((line + new_line))
-EOF
-}
-
-# Cleanup after running each script.
-cleanup() {
- (
- set +ex
-
- if command -v cleanup_hook 2>/dev/null; then
- echo >&2 "Cleanup hook found, calling..."
- cleanup_hook
- fi
-
- stop_all_tincs
- ) || 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=$(normalize_path "$SHELL")
-fi
-
-# This was called from a tincd script. Skip executing commands with side effects.
-[ -n "$NAME" ] && return
-
-echo [STEP] Check for leftover tinc daemons and test directories
-
-# Cleanup leftovers from previous runs.
-stop_all_tincs
-
-rm -rf "$DIR_FOO" "$DIR_BAR" "$DIR_BAZ"
-
-# Register cleanup function so we don't have to call it everywhere
-# (and failed scripts do not leave stray tincd running).
-trap cleanup EXIT INT TERM
--- /dev/null
+"""Testing library with a few helper classes and functions for use in tinc integration tests."""
+
+import sys
+
+assert sys.version_info >= (3, 6)
--- /dev/null
+"""Simple assertions which print the expected and received values on failure."""
+
+import typing as T
+
+from .log import log
+
+Val = T.TypeVar("Val")
+Num = T.TypeVar("Num", int, float)
+
+
+def false(value: T.Any) -> None:
+ """Check that value is falsy."""
+ if value:
+ raise ValueError(f'expected "{value}" to be falsy')
+
+
+def true(value: T.Any) -> None:
+ """Check that value is truthy."""
+ if not value:
+ raise ValueError(f'expected "{value}" to be truthy', value)
+
+
+def equals(expected: Val, actual: Val) -> None:
+ """Check that the two values are equal."""
+ if expected != actual:
+ raise ValueError(f'expected "{expected}", got "{actual}"')
+
+
+def has_prefix(text: T.AnyStr, prefix: T.AnyStr) -> None:
+ """Check that text has prefix."""
+ if not text.startswith(prefix):
+ raise ValueError(f"expected {text!r} to start with {prefix!r}")
+
+
+def in_range(value: Num, gte: Num, lte: Num) -> None:
+ """Check that value lies in the range [min, max]."""
+ if not gte >= value >= lte:
+ raise ValueError(f"value {value} must be between {gte} and {lte}")
+
+
+def is_in(needle: Val, *haystacks: T.Container[Val]) -> None:
+ """Check that at least one haystack includes needle."""
+ for haystack in haystacks:
+ if needle in haystack:
+ return
+ raise ValueError(f'expected any of "{haystacks}" to include "{needle}"')
+
+
+def not_in(needle: Val, *haystacks: T.Container[Val]) -> None:
+ """Check that all haystacks do not include needle."""
+ for haystack in haystacks:
+ if needle in haystack:
+ raise ValueError(f'expected all "{haystacks}" NOT to include "{needle}"')
+
+
+def nodes(node, want_nodes: int) -> None:
+ """Check that node can reach exactly N nodes (including itself)."""
+ log.debug("want %d reachable nodes from tinc %s", want_nodes, node)
+ stdout, _ = node.cmd("dump", "reachable", "nodes")
+ equals(want_nodes, len(stdout.splitlines()))
+
+
+def files_eq(path0: str, path1: str) -> None:
+ """Compare file contents, ignoring whitespace at both ends."""
+ log.debug("comparing files %s and %s", path0, path1)
+
+ def read(path: str) -> str:
+ log.debug("reading file %s", path)
+ with open(path, "r", encoding="utf-8") as f:
+ return f.read().strip()
+
+ content0 = read(path0)
+ content1 = read(path1)
+
+ if content0 != content1:
+ raise ValueError(f"expected files {path0} and {path1} to match")
--- /dev/null
+"""Wrappers for more complicated tinc/tincd commands."""
+
+import typing as T
+
+from . import check
+from .log import log
+from .proc import Tinc
+
+ExchangeIO = T.Tuple[
+ T.Tuple[str, str],
+ T.Tuple[str, str],
+ T.Tuple[str, str],
+]
+
+
+def exchange(node0: Tinc, node1: Tinc, export_all: bool = False) -> ExchangeIO:
+ """Run `export(-all) | exchange | import` between the passed nodes.
+ `export-all` is used if export_all is set to True.
+ """
+ export_cmd = "export-all" if export_all else "export"
+ log.debug("%s between %s and %s", export_cmd, node0.name, node1.name)
+
+ exp_out, exp_err = node0.cmd(export_cmd)
+ log.debug(
+ 'exchange: %s %s returned ("%s", "%s")', export_cmd, node0, exp_out, exp_err
+ )
+ check.is_in("Name =", exp_out)
+
+ xch_out, xch_err = node1.cmd("exchange", stdin=exp_out)
+ log.debug('exchange: exchange %s returned ("%s", "%s")', node1, xch_out, xch_err)
+ check.is_in("Name =", xch_out)
+ check.is_in("Imported ", xch_err)
+
+ imp_out, imp_err = node0.cmd("import", stdin=xch_out)
+ log.debug('exchange: import %s returned ("%s", "%s")', node0, imp_out, imp_err)
+ check.is_in("Imported ", imp_err)
+
+ return (
+ (exp_out, exp_err),
+ (xch_out, xch_err),
+ (imp_out, imp_err),
+ )
+
+
+def get(tinc: Tinc, var: str) -> str:
+ """Get the value of the variable, stripped of whitespace."""
+ assert var
+ stdout, _ = tinc.cmd("get", var)
+ return stdout.strip()
--- /dev/null
+"""Some hardcoded constants."""
+
+import os
+
+# Exit code to skip current test
+EXIT_SKIP = 77
+
+# Family name for multiprocessing Listener/Connection
+MPC_FAMILY = "AF_PIPE" if os.name == "nt" else "AF_UNIX"
--- /dev/null
+"""Classes for doing data exchange between test and tincd scripts."""
+
+import os
+import sys
+import time
+import platform
+import typing as T
+
+_MONOTONIC_IS_SYSTEMWIDE = not (
+ platform.system() == "Darwin" and sys.version_info < (3, 10)
+)
+
+
+def _time_ns() -> int:
+ if sys.version_info <= (3, 7):
+ return int(time.monotonic() * 1e9)
+ return time.monotonic_ns()
+
+
+class Notification:
+ """Notification about tinc script execution."""
+
+ test: str
+ node: str
+ script: str
+ created_at: T.Optional[int] = None
+ env: T.Dict[str, str]
+ args: T.Dict[str, str]
+ error: T.Optional[Exception]
+
+ def __init__(self) -> None:
+ self.env = dict(os.environ)
+
+ # This field is used to record when the notification was created. On most
+ # operating systems, it uses system-wide monotonic time which is the same
+ # for all processes. Not on macOS, at least not before Python 3.10. So if
+ # we're running such a setup, assign time local to our test process right
+ # when we receive the notification to have a common reference point to
+ # all measurements.
+ if _MONOTONIC_IS_SYSTEMWIDE:
+ self.update_time()
+
+ def update_time(self) -> None:
+ """Update creation time if it was not assigned previously."""
+ if self.created_at is None:
+ self.created_at = _time_ns()
--- /dev/null
+"""Wrappers for running external commands."""
+
+import subprocess as subp
+import atexit
+import typing as T
+
+from .log import log
+
+_netns_created: T.Set[str] = set()
+
+
+def _netns_cleanup() -> None:
+ for namespace in _netns_created.copy():
+ netns_delete(namespace)
+
+
+atexit.register(_netns_cleanup)
+
+
+def _netns_action(action: str, namespace: str) -> bool:
+ log.debug("%s network namespace %s", action, namespace)
+
+ res = subp.run(["ip", "netns", action, namespace], check=False)
+ if res.returncode:
+ log.error("could not %s netns %s", action, namespace)
+ else:
+ log.debug("OK %s netns %s", action, namespace)
+
+ return not res.returncode
+
+
+def netns_delete(namespace: str) -> bool:
+ """Remove a previously created network namespace."""
+ success = _netns_action("delete", namespace)
+ if success:
+ _netns_created.remove(namespace)
+ return success
+
+
+def netns_add(namespace: str) -> bool:
+ """Add a network namespace (which can be removed manually or automatically at exit)."""
+ success = _netns_action("add", namespace)
+ if success:
+ _netns_created.add(namespace)
+ return success
--- /dev/null
+"""Global logger for using in test and tincd scripts."""
+
+import logging
+import os
+import sys
+import typing as T
+from types import TracebackType
+
+from .path import TEST_WD, TEST_NAME
+
+logging.basicConfig(level=logging.DEBUG)
+
+_fmt = logging.Formatter(
+ "%(asctime)s %(name)s %(filename)s:%(lineno)d %(levelname)s %(message)s"
+)
+
+# Where to put log files for this test and nodes started by it
+_log_dir = os.path.join(TEST_WD, "logs")
+
+
+def new_logger(name: str) -> logging.Logger:
+ """Create a new named logger with common logging format.
+ Log entries will go into a separate logfile named 'name.log'.
+ """
+ os.makedirs(_log_dir, exist_ok=True)
+
+ logger = logging.getLogger(name)
+ logger.setLevel(logging.DEBUG)
+
+ file = logging.FileHandler(os.path.join(_log_dir, name + ".log"))
+ file.setFormatter(_fmt)
+ logger.addHandler(file)
+
+ return logger
+
+
+# Main logger used by most tests
+log = new_logger(TEST_NAME)
+
+
+def _exc_hook(
+ ex_type: T.Type[BaseException],
+ base: BaseException,
+ tb_type: T.Optional[TracebackType],
+) -> None:
+ """Logging handler for uncaught exceptions."""
+ log.error("Uncaught exception", exc_info=(ex_type, base, tb_type))
+
+
+sys.excepthook = _exc_hook
--- /dev/null
+"""Support for receiving notifications from tincd scripts."""
+
+import os
+import signal
+import threading
+import queue
+import multiprocessing.connection as mp
+import typing as T
+
+from .log import log
+from .event import Notification
+from .const import MPC_FAMILY
+
+
+def _get_key(name, script) -> str:
+ return f"{name}/{script}"
+
+
+class NotificationServer:
+ """Receive event notifications from tincd scripts."""
+
+ address: T.Union[str, bytes]
+ authkey: bytes # only to prevent accidental connections to wrong servers
+ _lock: threading.Lock
+ _ready: threading.Event
+ _worker: T.Optional[threading.Thread]
+ _notifications: T.Dict[str, queue.Queue]
+
+ def __init__(self) -> None:
+ self.address = ""
+ self.authkey = os.urandom(8)
+ self._lock = threading.Lock()
+ self._ready = threading.Event()
+ self._worker = threading.Thread(target=self._recv, daemon=True)
+ self._notifications = {}
+
+ log.debug("using authkey %s", self.authkey)
+
+ self._worker.start()
+ log.debug("waiting for notification worker to become ready")
+
+ self._ready.wait()
+ log.debug("notification worker is ready")
+
+ @T.overload
+ def get(self, node: str, script: str) -> Notification:
+ """Receive notification from the specified node and script without a timeout.
+ Doesn't return until a notification arrives.
+ """
+ return self.get(node, script)
+
+ @T.overload
+ def get(self, node: str, script: str, timeout: float) -> T.Optional[Notification]:
+ """Receive notification from the specified node and script with a timeout.
+ If nothing arrives before it expires, None is returned.
+ """
+ return self.get(node, script, timeout)
+
+ def get(
+ self, node: str, script: str, timeout: T.Optional[float] = None
+ ) -> T.Optional[Notification]:
+ """Receive notification from specified node and script. See overloads above."""
+
+ key = _get_key(node, script)
+ with self._lock:
+ que = self._notifications.get(key, queue.Queue())
+ self._notifications[key] = que
+ try:
+ return que.get(timeout=timeout)
+ except queue.Empty:
+ return None
+
+ def _recv(self) -> None:
+ try:
+ self._listen()
+ except (OSError, AssertionError) as ex:
+ log.error("recv notifications failed", exc_info=ex)
+ os.kill(0, signal.SIGTERM)
+
+ def _listen(self) -> None:
+ with mp.Listener(family=MPC_FAMILY, authkey=self.authkey) as listener:
+ assert not isinstance(listener.address, tuple)
+ self.address = listener.address
+ self._ready.set()
+ while True:
+ with listener.accept() as conn:
+ self._handle_conn(conn)
+
+ def _handle_conn(self, conn: mp.Connection) -> None:
+ log.debug("accepted connection")
+
+ data: Notification = conn.recv()
+ assert isinstance(data, Notification)
+ data.update_time()
+
+ key = _get_key(data.node, data.script)
+ log.debug('from "%s" received data "%s"', key, data)
+
+ with self._lock:
+ que = self._notifications.get(key, queue.Queue())
+ self._notifications[key] = que
+ que.put(data)
+
+
+notifications = NotificationServer()
--- /dev/null
+"""Paths to compiled binaries, and a few other important environment variables."""
+
+import os
+import pathlib
+import sys
+
+env = {
+ "TEST_NAME": os.getenv("TEST_NAME"),
+ "TINC_PATH": os.getenv("TINC_PATH"),
+ "TINCD_PATH": os.getenv("TINCD_PATH"),
+ "SPLICE_PATH": os.getenv("SPLICE_PATH"),
+ "PYTHON_PATH": os.getenv("PYTHON_PATH"),
+ "SPTPS_TEST_PATH": os.getenv("SPTPS_TEST_PATH"),
+ "SPTPS_KEYPAIR_PATH": os.getenv("SPTPS_KEYPAIR_PATH"),
+}
+
+# Not strictly necessary, used for better autocompletion and search by reference.
+TEST_NAME = str(env["TEST_NAME"])
+TINC_PATH = str(env["TINC_PATH"])
+TINCD_PATH = str(env["TINCD_PATH"])
+SPLICE_PATH = str(env["SPLICE_PATH"])
+PYTHON_PATH = str(env["PYTHON_PATH"])
+SPTPS_TEST_PATH = str(env["SPTPS_TEST_PATH"])
+SPTPS_KEYPAIR_PATH = str(env["SPTPS_KEYPAIR_PATH"])
+
+
+def _check() -> bool:
+ """Basic sanity checks on passed environment variables."""
+ for key, val in env.items():
+ if not val or (key != "TEST_NAME" and not os.path.isfile(val)):
+ return False
+ return True
+
+
+if not _check():
+ MSG = """
+Please run tests using
+ $ meson test -C build
+or
+ $ ninja -C build test
+"""
+ print(MSG, file=sys.stderr)
+ sys.exit(1)
+
+# Current working directory
+CWD = os.getcwd()
+
+# Path to the testing library
+TESTLIB_ROOT = pathlib.Path(__file__).parent
+
+# Source root for the integration test suite
+TEST_SRC_ROOT = TESTLIB_ROOT.parent.resolve()
+
+_wd = os.path.join(CWD, "wd")
+os.makedirs(_wd, exist_ok=True)
+
+# Useful when running tests manually
+_gitignore = os.path.join(_wd, ".gitignore")
+if not os.path.exists(_gitignore):
+ with open(_gitignore, "w", encoding="utf-8") as f:
+ f.write("*")
+
+# Working directory for this test
+TEST_WD = os.path.join(_wd, TEST_NAME)
--- /dev/null
+"""Classes for working with compiled instances of tinc and tincd binaries."""
+
+import os
+import random
+import typing as T
+import subprocess as subp
+from enum import Enum
+from platform import system
+
+from . import check, path
+from .log import log
+from .script import TincScript, Script, ScriptType
+from .template import make_script, make_cmd_wrap
+from .util import random_string, random_port
+
+# Does the OS support all addresses in 127.0.0.0/8 without additional configuration?
+_FULL_LOCALHOST_SUBNET = system() in ("Linux", "Windows")
+
+
+def _make_wd(name: str) -> str:
+ work_dir = os.path.join(path.TEST_WD, "data", name)
+ os.makedirs(work_dir, exist_ok=True)
+ return work_dir
+
+
+def _random_octet() -> int:
+ return random.randint(1, 254)
+
+
+def _rand_localhost() -> str:
+ """Generate random IP in subnet 127.0.0.0/8 for operating systems that support
+ it without additional configuration. For all others, return 127.0.0.1.
+ """
+ if _FULL_LOCALHOST_SUBNET:
+ return f"127.{_random_octet()}.{_random_octet()}.{_random_octet()}"
+ return "127.0.0.1"
+
+
+class Feature(Enum):
+ """Optional features supported by both tinc and tincd."""
+
+ COMP_LZ4 = "comp_lz4"
+ COMP_LZO = "comp_lzo"
+ COMP_ZLIB = "comp_zlib"
+ CURSES = "curses"
+ JUMBOGRAMS = "jumbograms"
+ LEGACY_PROTOCOL = "legacy_protocol"
+ LIBGCRYPT = "libgcrypt"
+ MINIUPNPC = "miniupnpc"
+ OPENSSL = "openssl"
+ READLINE = "readline"
+ TUNEMU = "tunemu"
+ UML = "uml"
+ VDE = "vde"
+
+
+class Tinc:
+ """Thin wrapper around Popen that simplifies running tinc/tincd
+ binaries by passing required arguments, checking exit codes, etc.
+ """
+
+ name: str
+ address: str
+ _work_dir: str
+ _port: T.Optional[int]
+ _scripts: T.Dict[str, TincScript]
+ _procs: T.List[subp.Popen]
+
+ def __init__(self, name: str = "", addr: str = "") -> None:
+ self.name = name if name else random_string(10)
+ self.address = addr if addr else _rand_localhost()
+ self._work_dir = _make_wd(self.name)
+ self._port = None
+ self._scripts = {}
+ self._procs = []
+
+ def randomize_port(self) -> int:
+ """Use a random port for this node."""
+ self._port = random_port()
+ return self._port
+
+ def read_port(self) -> int:
+ """Read port used by tincd from its pidfile and update the _port field."""
+ pidfile = self.sub("pid")
+ log.debug("reading pidfile at %s", pidfile)
+
+ with open(pidfile, "r", encoding="utf-8") as f:
+ content = f.read()
+ log.debug("found data %s", content)
+
+ _, _, _, token, port = content.split()
+ check.equals("port", token)
+
+ self._port = int(port)
+ return self._port
+
+ @property
+ def port(self) -> int:
+ """Port that tincd is listening on."""
+ assert self._port is not None
+ return self._port
+
+ def __str__(self) -> str:
+ return self.name
+
+ def __getitem__(self, script: ScriptType) -> TincScript:
+ if isinstance(script, Script):
+ script = script.name
+ return self._scripts[script]
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.cleanup()
+
+ @property
+ def features(self) -> T.List[Feature]:
+ """List of features supported by tinc and tincd."""
+ tinc, _ = self.cmd("--version")
+ tincd, _ = self.tincd("--version").communicate(timeout=5)
+ prefix, features = "Features: ", []
+
+ for out in tinc, tincd:
+ for line in out.splitlines():
+ if not line.startswith(prefix):
+ continue
+ tokens = line[len(prefix) :].split()
+ for token in tokens:
+ features.append(Feature(token))
+ break
+
+ log.info('supported features: "%s"', features)
+ return features
+
+ @property
+ def _common_args(self) -> T.List[str]:
+ return [
+ "--net",
+ self.name,
+ "--config",
+ self._work_dir,
+ "--pidfile",
+ self.sub("pid"),
+ ]
+
+ def sub(self, *paths: str) -> str:
+ """Return path to a subdirectory within the working dir for this node."""
+ return os.path.join(self._work_dir, *paths)
+
+ @property
+ def script_up(self) -> str:
+ """Name of the hosts/XXX-up script for this node."""
+ return f"hosts/{self.name}-up"
+
+ @property
+ def script_down(self) -> str:
+ """Name of the hosts/XXX-down script for this node."""
+ return f"hosts/{self.name}-down"
+
+ def cleanup(self) -> None:
+ """Terminate all tinc and tincd processes started from this instance."""
+ log.info("running node cleanup for %s", self)
+
+ try:
+ self.cmd("stop")
+ except (AssertionError, ValueError):
+ log.info("unsuccessfully tried to stop node %s", self)
+
+ for proc in self._procs:
+ if proc.returncode is not None:
+ log.debug("PID %d exited, skipping", proc.pid)
+ else:
+ log.info("PID %d still running, stopping", proc.pid)
+ try:
+ proc.kill()
+ except OSError as ex:
+ log.error("could not kill PID %d", proc.pid, exc_info=ex)
+
+ log.debug("waiting on %d to prevent zombies", proc.pid)
+ try:
+ proc.wait()
+ except OSError as ex:
+ log.error("waiting on %d failed", proc.pid, exc_info=ex)
+
+ self._procs.clear()
+
+ def start(self, *args: str) -> int:
+ """Start the node, wait for it to call tinc-up, and get the port it's
+ listening on from the pid file. Don't use this method unless you need
+ to know the port tincd is running on. Call .cmd("start"), it's faster.
+
+ Reading pidfile and setting the port cannot be done from tinc-up because
+ you can't send tinc commands to yourself there — the daemon doesn't
+ respond to them until tinc-up is finished. The port field on this Tinc
+ instance is updated to reflect the correct port. If tinc-up is missing,
+ this command creates a new one, and then disables it.
+ """
+ new_script = Script.TINC_UP.name not in self._scripts
+ if new_script:
+ self.add_script(Script.TINC_UP)
+
+ tinc_up = self[Script.TINC_UP]
+ self.cmd(*args, "start")
+ tinc_up.wait()
+
+ if new_script:
+ tinc_up.disable()
+
+ self._port = self.read_port()
+ self.cmd("set", "Port", str(self._port))
+
+ return self._port
+
+ def cmd(
+ self, *args: str, code: T.Optional[int] = 0, stdin: T.Optional[str] = None
+ ) -> T.Tuple[str, str]:
+ """Run command through tinc, writes `stdin` to it (if the argument is not None),
+ check its return code (if the argument is not None), and return (stdout, stderr).
+ """
+ proc = self.tinc(*args)
+ log.debug('tinc %s: PID %d, in "%s", want code %s', self, proc.pid, stdin, code)
+
+ out, err = proc.communicate(stdin, timeout=60)
+ res = proc.returncode
+ self._procs.remove(proc)
+ log.debug('tinc %s: code %d, out "%s", err "%s"', self, res, out, err)
+
+ if code is not None:
+ check.equals(code, res)
+
+ # Check that port was not used by something else
+ check.not_in("Can't bind to ", err)
+
+ return out if out else "", err if err else ""
+
+ def tinc(self, *args: str) -> subp.Popen:
+ """Start tinc with the specified arguments."""
+ args = tuple(filter(bool, args))
+ cmd = [path.TINC_PATH, *self._common_args, *args]
+ log.debug('starting tinc %s: "%s"', self.name, " ".join(cmd))
+ # pylint: disable=consider-using-with
+ proc = subp.Popen(
+ cmd,
+ cwd=self._work_dir,
+ stdin=subp.PIPE,
+ stdout=subp.PIPE,
+ stderr=subp.PIPE,
+ encoding="utf-8",
+ )
+ self._procs.append(proc)
+ return proc
+
+ def tincd(self, *args: str, env: T.Optional[T.Dict[str, str]] = None) -> subp.Popen:
+ """Start tincd with the specified arguments."""
+ args = tuple(filter(bool, args))
+ cmd = [
+ path.TINCD_PATH,
+ *self._common_args,
+ "--logfile",
+ self.sub("log"),
+ "-d5",
+ *args,
+ ]
+ log.debug('starting tincd %s: "%s"', self.name, " ".join(cmd))
+ if env is not None:
+ env = {**os.environ, **env}
+ # pylint: disable=consider-using-with
+ proc = subp.Popen(
+ cmd,
+ cwd=self._work_dir,
+ stdin=subp.PIPE,
+ stdout=subp.PIPE,
+ stderr=subp.PIPE,
+ encoding="utf-8",
+ env=env,
+ )
+ self._procs.append(proc)
+ return proc
+
+ def add_script(self, script: ScriptType, source: str = "") -> TincScript:
+ """Create a script with the passed Python source code.
+ The source must either be empty, or start indentation with 4 spaces.
+ If the source is empty, the created script can be used to receive notifications.
+ """
+ rel_path = script if isinstance(script, str) else script.value
+ check.not_in(rel_path, self._scripts)
+
+ full_path = os.path.join(self._work_dir, rel_path)
+ tinc_script = TincScript(self.name, rel_path, full_path)
+
+ log.debug("creating script %s at %s", script, full_path)
+ with open(full_path, "w", encoding="utf-8") as f:
+ content = make_script(self.name, rel_path, source)
+ f.write(content)
+
+ if os.name == "nt":
+ log.debug("creating .cmd script wrapper at %s", full_path)
+ win_content = make_cmd_wrap(full_path)
+ with open(f"{full_path}.cmd", "w", encoding="utf-8") as f:
+ f.write(win_content)
+ else:
+ os.chmod(full_path, 0o755)
+
+ if isinstance(script, Script):
+ self._scripts[script.name] = tinc_script
+ self._scripts[rel_path] = tinc_script
+
+ return tinc_script
--- /dev/null
+"""Classes related to creation and control of tincd scripts."""
+
+import os
+import typing as T
+from enum import Enum
+
+from .log import log
+from .event import Notification
+from .notification import notifications
+
+
+class Script(Enum):
+ """A list of supported tincd scripts.
+ hosts/XXX-{up,down} are missing because we generate node names at runtime.
+ """
+
+ TINC_UP = "tinc-up"
+ TINC_DOWN = "tinc-down"
+ HOST_UP = "host-up"
+ HOST_DOWN = "host-down"
+ SUBNET_UP = "subnet-up"
+ SUBNET_DOWN = "subnet-down"
+ INVITATION_CREATED = "invitation-created"
+ INVITATION_ACCEPTED = "invitation-accepted"
+
+
+# Since we rely on dynamically created node names, we cannot put 'hosts/XXX-up' in an enum.
+# This is the reason we sometimes need strings to type script variables.
+ScriptType = T.Union[Script, str]
+
+
+class TincScript:
+ """Control created tincd scripts and receive notifications from them."""
+
+ _node: str
+ _path: str
+ _script: str
+
+ def __init__(self, node: str, script: str, path: str) -> None:
+ self._node = node
+ self._script = script
+ self._path = path
+
+ def __str__(self):
+ return f"{self._node}/{self._script}"
+
+ @T.overload
+ def wait(self) -> Notification:
+ """Wait for the script to finish, returning the notification sent by the script."""
+ return self.wait()
+
+ @T.overload
+ def wait(self, timeout: float) -> T.Optional[Notification]:
+ """Wait for the script to finish, returning the notification sent by the script.
+ If nothing arrives before timeout expires, None is returned."""
+ return self.wait(timeout)
+
+ def wait(self, timeout: T.Optional[float] = None) -> T.Optional[Notification]:
+ """Wait for the script to finish. See overloads above."""
+ log.debug("waiting for script %s/%s", self._node, self._script)
+ if timeout is None:
+ return notifications.get(self._node, self._script)
+ return notifications.get(self._node, self._script, timeout)
+
+ @property
+ def enabled(self) -> bool:
+ """Check if script is enabled."""
+ if os.name == "nt":
+ return os.path.exists(self._path)
+ return os.access(self._path, os.X_OK)
+
+ def disable(self) -> None:
+ """Disable the script by renaming it."""
+ log.debug("disabling script %s/%s", self._node, self._script)
+ assert self.enabled
+ os.rename(self._path, self._disabled_name)
+
+ def enable(self) -> None:
+ """Enable the script by renaming it back."""
+ log.debug("enabling script %s/%s", self._node, self._script)
+ assert not self.enabled
+ os.rename(self._disabled_name, self._path)
+
+ @property
+ def _disabled_name(self) -> str:
+ return f"{self._path}.disabled"
--- /dev/null
+"""Various script and configuration file templates."""
+
+import os
+import typing as T
+from string import Template
+
+from . import path
+from .notification import notifications
+
+
+_CMD_VARS = os.linesep.join([f"set {var}={val}" for var, val in path.env.items()])
+_CMD_PY = "runpython" if "meson.exe" in path.PYTHON_PATH.lower() else ""
+
+
+def _read_template(tpl_name: str, maps: T.Dict[str, T.Any]) -> str:
+ tpl_path = path.TESTLIB_ROOT.joinpath("template", tpl_name)
+ tpl = Template(tpl_path.read_text(encoding="utf-8"))
+ return tpl.substitute(maps)
+
+
+def make_script(node: str, script: str, source: str) -> str:
+ """Create a tincd script."""
+ addr = notifications.address
+ if isinstance(addr, str):
+ addr = f'r"{addr}"' # 'r' is for Windows pipes: \\.\foo\bar
+ maps = {
+ "AUTH_KEY": notifications.authkey,
+ "CWD": path.CWD,
+ "NODE_NAME": node,
+ "NOTIFICATIONS_ADDR": addr,
+ "PYTHON_PATH": path.PYTHON_PATH,
+ "SCRIPT_NAME": script,
+ "SCRIPT_SOURCE": source,
+ "SRC_ROOT": path.TEST_SRC_ROOT,
+ "TEST_NAME": path.TEST_NAME,
+ }
+ return _read_template("script.py.tpl", maps)
+
+
+def make_cmd_wrap(script: str) -> str:
+ """Create a .cmd wrapper for tincd script. Only makes sense on Windows."""
+ maps = {
+ "PYTHON_CMD": _CMD_PY,
+ "PYTHON_PATH": path.PYTHON_PATH,
+ "SCRIPT_PATH": script,
+ "VARIABLES": _CMD_VARS,
+ }
+ return _read_template("script.cmd.tpl", maps)
+
+
+def make_netns_config(namespace: str, ip_addr: str, mask: int) -> str:
+ """Create a tincd script that does network namespace configuration."""
+ maps = {"NAMESPACE": namespace, "ADDRESS": ip_addr, "MASK": mask}
+ return _read_template("netns.py.tpl", maps)
--- /dev/null
+ # Indentation is important! This gets copied inside another Python script.
+ import subprocess as subp
+
+ iface = os.environ['INTERFACE']
+ log.info('using interface %s', iface)
+
+ subp.run(['ip', 'link', 'set', 'dev', iface, 'netns', '$NAMESPACE'], check=True)
+ subp.run(['ip', 'netns', 'exec', '$NAMESPACE', 'ip', 'addr', 'add', '$ADDRESS/$MASK', 'dev', iface], check=True)
+ subp.run(['ip', 'netns', 'exec', '$NAMESPACE', 'ip', 'link', 'set', iface, 'up'], check=True)
--- /dev/null
+@echo off
+$VARIABLES
+"$PYTHON_PATH" $PYTHON_CMD "$SCRIPT_PATH"
--- /dev/null
+#!$PYTHON_PATH
+
+import os
+import sys
+import multiprocessing.connection as mpc
+import typing as T
+import time
+import signal
+
+def on_error(*args):
+ try:
+ log.error('Uncaught exception', exc_info=args)
+ except NameError:
+ print('Uncaught exception', args)
+ os.kill(0, signal.SIGTERM)
+
+sys.excepthook = on_error
+
+os.chdir(r'$CWD')
+sys.path.append(r'$SRC_ROOT')
+
+from testlib.proc import Tinc
+from testlib.event import Notification
+from testlib.log import new_logger
+from testlib.const import MPC_FAMILY
+
+this = Tinc('$NODE_NAME')
+log = new_logger(this.name)
+
+def notify_test(args: T.Dict[str, T.Any] = {}, error: T.Optional[Exception] = None):
+ log.debug(f'sending notification to %s', $NOTIFICATIONS_ADDR)
+
+ evt = Notification()
+ evt.test = '$TEST_NAME'
+ evt.node = '$NODE_NAME'
+ evt.script = '$SCRIPT_NAME'
+ evt.args = args
+ evt.error = error
+
+ for retry in range(1, 10):
+ try:
+ with mpc.Client($NOTIFICATIONS_ADDR, family=MPC_FAMILY, authkey=$AUTH_KEY) as conn:
+ conn.send(evt)
+ log.debug(f'sent notification')
+ break
+ except Exception as ex:
+ log.error(f'notification failed', exc_info=ex)
+ time.sleep(0.5)
+
+try:
+ log.debug('running user code')
+$SCRIPT_SOURCE
+ log.debug('user code finished')
+except Exception as ex:
+ log.error('user code failed', exc_info=ex)
+ notify_test(error=ex)
+ sys.exit(1)
+
+notify_test()
--- /dev/null
+"""Test context that wraps Tinc instances and terminates them on exit."""
+
+import typing as T
+
+from .log import log
+from .proc import Tinc
+
+
+class Test:
+ """Test context. Allows you to obtain Tinc instances which are automatically
+ stopped (and killed if necessary) at __exit__. Should be wrapped in `with`
+ statements (like the built-in `open`). Should be used sparingly (as it usually
+ happens, thanks to Windows: service registration and removal is quite slow,
+ which makes tests take a long time to run, especially on modest CI VMs).
+ """
+
+ name: str
+ _nodes: T.List[Tinc]
+
+ def __init__(self, name: str) -> None:
+ self._nodes = []
+ self.name = name
+
+ def node(self, addr: str = "") -> Tinc:
+ """Create a Tinc instance and remember it for termination on exit."""
+ node = Tinc(addr=addr)
+ self._nodes.append(node)
+ return node
+
+ def __str__(self) -> str:
+ return self.name
+
+ def __enter__(self) -> "Test":
+ log.info("RUNNING TEST: %s", self.name)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ for node in self._nodes:
+ node.cleanup()
+ log.info("FINISHED TEST: %s", self.name)
--- /dev/null
+"""Miscellaneous utility functions."""
+
+import os
+import sys
+import subprocess as subp
+import random
+import string
+import socket
+import typing as T
+
+from . import check
+from .log import log
+from .const import EXIT_SKIP
+
+_ALPHA_NUMERIC = string.ascii_lowercase + string.digits
+
+
+def random_port() -> int:
+ """Return an unused TCP port in the unprivileged range.
+ Note that this function releases the port before returning, and it can be
+ overtaken by something else before you use it.
+ """
+ while True:
+ port = random.randint(1024, 65535)
+ try:
+ with socket.socket() as sock:
+ sock.bind(("0.0.0.0", port))
+ sock.listen()
+ return port
+ except OSError as ex:
+ log.debug("could not bind to random port %d", port, exc_info=ex)
+
+
+def random_string(k: int) -> str:
+ """Generate a random alphanumeric string of length k."""
+ return "".join(random.choices(_ALPHA_NUMERIC, k=k))
+
+
+def find_line(filename: str, prefix: str) -> str:
+ """Find a line with the prefix in a text file.
+ Check that only one line matches.
+ """
+ with open(filename, "r", encoding="utf-8") as f:
+ keylines = [line for line in f.readlines() if line.startswith(prefix)]
+ check.equals(1, len(keylines))
+ return keylines[0].rstrip()
+
+
+def require_root() -> None:
+ """Check that test is running with root privileges.
+ Exit with code 77 otherwise.
+ """
+ euid = os.geteuid()
+ if euid:
+ log.info("this test requires root (but running under UID %d)", euid)
+ sys.exit(EXIT_SKIP)
+
+
+def require_command(*args: str) -> None:
+ """Check that command args runs with exit code 0.
+ Exit with code 77 otherwise.
+ """
+ if subp.run(args, check=False).returncode:
+ log.info('this test requires command "%s" to work', " ".join(args))
+ sys.exit(EXIT_SKIP)
+
+
+def require_path(path: str) -> None:
+ """Check that path exists in your file system.
+ Exit with code 77 otherwise.
+ """
+ if not os.path.exists(path):
+ log.warning("this test requires path %s to be present", path)
+ sys.exit(EXIT_SKIP)
+
+
+# Thin wrappers around `with open(...) as f: f.do_something()`
+# Don't do much, besides saving quite a bit of space because of how frequently they're needed.
+
+
+def read_text(path: str) -> str:
+ """Return the text contents of a file."""
+ with open(path, encoding="utf-8") as f:
+ return f.read()
+
+
+def write_text(path: str, text: str) -> str:
+ """Write text to a file, replacing its content. Return the text added."""
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(text)
+ return text
+
+
+def read_lines(path: str) -> T.List[str]:
+ """Read file as a list of lines."""
+ with open(path, encoding="utf-8") as f:
+ return f.read().splitlines()
+
+
+def write_lines(path: str, lines: T.List[str]) -> T.List[str]:
+ """Write text lines to a file, replacing it content. Return the line added."""
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(os.linesep.join(lines))
+ f.write(os.linesep)
+ return lines
+
+
+def append_line(path: str, line: str) -> str:
+ """Append a line to the end of the file. Return the line added."""
+ line = f"{os.linesep}{line}{os.linesep}"
+ with open(path, "a", encoding="utf-8") as f:
+ f.write(line)
+ return line
--- /dev/null
+#!/usr/bin/env python3
+
+"""Test tinc and tincd configuration variables."""
+
+import typing as T
+from pathlib import Path
+
+from testlib import check, cmd
+from testlib.log import log
+from testlib.proc import Tinc
+from testlib.test import Test
+
+bad_subnets = (
+ "1.1.1",
+ "1:2:3:4:5:",
+ "1:2:3:4:5:::6",
+ "1:2:3:4:5:6:7:8:9",
+ "256.256.256.256",
+ "1:2:3:4:5:6:7:8.123",
+ "1:2:3:4:5:6:7:1.2.3.4",
+ "a:b:c:d:e:f:g:h",
+ "1.1.1.1/0",
+ "1.1.1.1/-1",
+ "1.1.1.1/33",
+ "1::/0",
+ "1::/-1",
+ "1::/129",
+ ":" * 1024,
+)
+
+
+def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
+ """Initialize new test nodes."""
+ node0, node1 = ctx.node(), ctx.node()
+
+ log.info("initialize node %s", node0)
+ stdin = f"""
+ init {node0}
+ set Port 0
+ set Address localhost
+ get Name
+ """
+ out, _ = node0.cmd(stdin=stdin)
+ check.equals(node0.name, out.strip())
+
+ return node0, node1
+
+
+with Test("test case sensitivity") as context:
+ foo, bar = init(context)
+
+ foo.cmd("set", "Mode", "switch")
+ check.equals("switch", cmd.get(foo, "Mode"))
+ check.equals("switch", cmd.get(foo, "mOdE"))
+
+ foo.cmd("set", "Mode", "router")
+ check.equals("router", cmd.get(foo, "MoDE"))
+ check.equals("router", cmd.get(foo, "mode"))
+
+ foo.cmd("set", "Mode", "Switch")
+ check.equals("Switch", cmd.get(foo, "mode"))
+
+ foo.cmd("del", "Mode", "hub", code=1)
+ foo.cmd("del", "Mode", "switch")
+ mode, _ = foo.cmd("get", "Mode", code=1)
+ check.false(mode)
+
+with Test("single Mode variable is permitted") as context:
+ foo, bar = init(context)
+ foo.cmd("add", "Mode", "switch")
+ foo.cmd("add", "Mode", "hub")
+ check.equals("hub", cmd.get(foo, "Mode"))
+
+with Test("test addition/deletion of multivalued variables") as context:
+ foo, bar = init(context)
+ for i in range(1, 4):
+ sub = f"{i}.{i}.{i}.{i}"
+ foo.cmd("add", "Subnet", sub)
+ foo.cmd("add", "Subnet", sub)
+ check.equals(["1.1.1.1", "2.2.2.2", "3.3.3.3"], cmd.get(foo, "Subnet").splitlines())
+
+ log.info("delete one subnet")
+ foo.cmd("del", "Subnet", "2.2.2.2")
+ check.equals(["1.1.1.1", "3.3.3.3"], cmd.get(foo, "Subnet").splitlines())
+
+ log.info("delete all subnets")
+ foo.cmd("del", "Subnet")
+ subnet, _ = foo.cmd("get", "Subnet", code=1)
+ check.false(subnet)
+
+with Test("cannot get/set server variables using node.variable syntax") as context:
+ foo, bar = init(context)
+ name, _ = foo.cmd("get", f"{foo.name}.Name", code=1)
+ check.false(name)
+ foo.cmd("set", f"{foo.name}.Name", "fake", code=1)
+
+with Test("get/set host variables for other nodes") as context:
+ foo, bar = init(context)
+ foo_bar = foo.sub("hosts", bar.name)
+ Path(foo_bar).touch(0o644, exist_ok=True)
+
+ bar_pmtu = f"{bar.name}.PMTU"
+ foo.cmd("add", bar_pmtu, "1")
+ foo.cmd("add", bar_pmtu, "2")
+ check.equals("2", cmd.get(foo, bar_pmtu))
+
+ bar_subnet = f"{bar.name}.Subnet"
+ for i in range(1, 4):
+ sub = f"{i}.{i}.{i}.{i}"
+ foo.cmd("add", bar_subnet, sub)
+ foo.cmd("add", bar_subnet, sub)
+
+ check.equals(
+ ["1.1.1.1", "2.2.2.2", "3.3.3.3"], cmd.get(foo, bar_subnet).splitlines()
+ )
+
+ foo.cmd("del", bar_subnet, "2.2.2.2")
+ check.equals(["1.1.1.1", "3.3.3.3"], cmd.get(foo, bar_subnet).splitlines())
+
+ foo.cmd("del", bar_subnet)
+ subnet, _ = foo.cmd("get", bar_subnet, code=1)
+ check.false(subnet)
+
+with Test("cannot get/set variables for nodes with invalid names") as context:
+ foo, bar = init(context)
+ Path(foo.sub("hosts", "fake-node")).touch(0o644, exist_ok=True)
+ foo.cmd("set", "fake-node.Subnet", "1.1.1.1", code=1)
+
+ log.info("cannot set obsolete variables unless forced")
+ foo.cmd("set", "PrivateKey", "12345", code=1)
+ foo.cmd("--force", "set", "PrivateKey", "67890")
+ check.equals("67890", cmd.get(foo, "PrivateKey"))
+
+ foo.cmd("del", "PrivateKey")
+ key, _ = foo.cmd("get", "PrivateKey", code=1)
+ check.false(key)
+
+ log.info("cannot set/add malformed Subnets")
+ for subnet in bad_subnets:
+ log.info("testing subnet %s", subnet)
+ foo.cmd("add", "Subnet", subnet, code=1)
+
+ subnet, _ = foo.cmd("get", "Subnet", code=1)
+ check.false(subnet)
+++ /dev/null
-#!/bin/sh
-
-# shellcheck disable=SC1090
-. "$TESTLIB_PATH"
-
-echo [STEP] Initialize one node
-
-tinc foo init foo
-test "$(tinc foo get Name)" = "foo"
-
-echo [STEP] Test case sensitivity
-
-tinc foo set Mode switch
-test "$(tinc foo get Mode)" = "switch"
-test "$(tinc foo get mode)" = "switch"
-
-tinc foo set mode router
-test "$(tinc foo get Mode)" = "router"
-test "$(tinc foo get mode)" = "router"
-
-tinc foo set Mode Switch
-test "$(tinc foo get Mode)" = "Switch"
-
-echo [STEP] Test deletion
-
-expect_code "$EXIT_FAILURE" tinc foo del Mode hub
-tinc foo del Mode switch
-test -z "$(tinc foo get Mode)"
-
-echo [STEP] There can only be one Mode variable
-
-tinc foo add Mode switch
-tinc foo add Mode hub
-test "$(tinc foo get Mode)" = "hub"
-
-echo [STEP] Test addition/deletion of multivalued variables
-
-tinc foo add Subnet 1.1.1.1
-tinc foo add Subnet 2.2.2.2
-tinc foo add Subnet 2.2.2.2
-tinc foo add Subnet 3.3.3.3
-test "$(tinc foo get Subnet | rm_cr)" = "1.1.1.1
-2.2.2.2
-3.3.3.3"
-
-tinc foo del Subnet 2.2.2.2
-test "$(tinc foo get Subnet | rm_cr)" = "1.1.1.1
-3.3.3.3"
-
-tinc foo del Subnet
-test -z "$(tinc foo get Subnet)"
-
-echo [STEP] We should not be able to get/set server variables using node.variable syntax
-
-test -z "$(tinc foo get foo.Name)"
-expect_code "$EXIT_FAILURE" tinc foo set foo.Name bar
-
-echo [STEP] Test getting/setting host variables for other nodes
-
-touch "$DIR_FOO/hosts/bar"
-
-tinc foo add bar.PMTU 1
-tinc foo add bar.PMTU 2
-test "$(tinc foo get bar.PMTU)" = "2"
-
-tinc foo add bar.Subnet 1.1.1.1
-tinc foo add bar.Subnet 2.2.2.2
-tinc foo add bar.Subnet 2.2.2.2
-tinc foo add bar.Subnet 3.3.3.3
-test "$(tinc foo get bar.Subnet | rm_cr)" = "1.1.1.1
-2.2.2.2
-3.3.3.3"
-
-tinc foo del bar.Subnet 2.2.2.2
-test "$(tinc foo get bar.Subnet | rm_cr)" = "1.1.1.1
-3.3.3.3"
-
-tinc foo del bar.Subnet
-test -z "$(tinc foo get bar.Subnet)"
-
-echo [STEP] We should not be able to get/set for nodes with invalid names
-
-touch "$DIR_FOO/hosts/qu-ux"
-expect_code "$EXIT_FAILURE" tinc foo set qu-ux.Subnet 1.1.1.1
-
-echo [STEP] We should not be able to set obsolete variables unless forced
-
-expect_code "$EXIT_FAILURE" tinc foo set PrivateKey 12345
-tinc foo --force set PrivateKey 12345
-test "$(tinc foo get PrivateKey)" = "12345"
-
-tinc foo del PrivateKey
-test -z "$(tinc foo get PrivateKey)"
-
-echo [STEP] We should not be able to set/add malformed Subnets
-
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1.1.1
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1:2:3:4:5:
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1:2:3:4:5:::6
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1:2:3:4:5:6:7:8:9
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 256.256.256.256
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1:2:3:4:5:6:7:8.123
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1:2:3:4:5:6:7:1.2.3.4
-expect_code "$EXIT_FAILURE" tinc foo add Subnet a:b:c:d:e:f:g:h
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1.1.1.1/0
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1.1.1.1/-1
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1.1.1.1/33
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1::/0
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1::/-1
-expect_code "$EXIT_FAILURE" tinc foo add Subnet 1::/129
-expect_code "$EXIT_FAILURE" tinc foo add Subnet ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-test -z "$(tinc foo get Subnet)"
-if cc_name != 'msvc'
- subdir('integration')
-endif
-
+subdir('integration')
subdir('unit')