From 2acfe91297824ce134dec04fc0751adc1f905feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juozapas=20Bo=C4=8Dkus?= Date: Fri, 8 Mar 2024 11:44:09 +0200 Subject: [PATCH] Add dtcli --- .unreleased/LLT-4746 | 0 Cargo.lock | 275 ++++++++++++------ clis/tcli/Cargo.toml | 13 + .../tcli/contrib/example_vpn_disable_linux.sh | 67 +++++ clis/tcli/contrib/example_vpn_enable_linux.sh | 117 ++++++++ clis/tcli/src/{main.rs => bin/tcli.rs} | 19 +- clis/tcli/src/bin/tclid/coms.rs | 142 +++++++++ clis/tcli/src/bin/tclid/daemon.rs | 114 ++++++++ .../src/bin/tclid/doc/tclid_documentation.md | 99 +++++++ clis/tcli/src/bin/tclid/main.rs | 191 ++++++++++++ clis/tcli/src/bin/tclid/tclid.rs | 58 ++++ .../tcli/src/bin/tclid/telio_cli_interface.rs | 78 +++++ clis/tcli/src/cli.rs | 108 +++---- clis/tcli/src/lib.rs | 3 + clis/tcli/src/nord.rs | 1 - test_runner.sh | 2 +- 16 files changed, 1142 insertions(+), 145 deletions(-) create mode 100644 .unreleased/LLT-4746 create mode 100755 clis/tcli/contrib/example_vpn_disable_linux.sh create mode 100755 clis/tcli/contrib/example_vpn_enable_linux.sh rename clis/tcli/src/{main.rs => bin/tcli.rs} (89%) create mode 100644 clis/tcli/src/bin/tclid/coms.rs create mode 100644 clis/tcli/src/bin/tclid/daemon.rs create mode 100644 clis/tcli/src/bin/tclid/doc/tclid_documentation.md create mode 100644 clis/tcli/src/bin/tclid/main.rs create mode 100644 clis/tcli/src/bin/tclid/tclid.rs create mode 100644 clis/tcli/src/bin/tclid/telio_cli_interface.rs create mode 100644 clis/tcli/src/lib.rs diff --git a/.unreleased/LLT-4746 b/.unreleased/LLT-4746 new file mode 100644 index 000000000..e69de29bb diff --git a/Cargo.lock b/Cargo.lock index bd6dbc1de..f64c975f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" @@ -107,10 +107,10 @@ dependencies = [ "basic-toml", "mime", "mime_guess", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "serde", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -293,9 +293,9 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -565,9 +565,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" dependencies = [ "jobserver", "libc", @@ -699,7 +699,7 @@ checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ "heck 0.4.1", "proc-macro-error", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", ] @@ -711,9 +711,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -849,9 +849,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -877,9 +877,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -989,9 +989,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", +] + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", ] [[package]] @@ -1022,7 +1031,7 @@ checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "strsim 0.10.0", "syn 1.0.109", @@ -1036,10 +1045,10 @@ checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "strsim 0.11.1", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -1061,7 +1070,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core 0.20.9", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -1092,7 +1101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" dependencies = [ "darling 0.12.4", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", ] @@ -1259,9 +1268,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -1279,9 +1288,9 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -1535,9 +1544,9 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -1611,9 +1620,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -2023,9 +2032,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2052,6 +2061,32 @@ dependencies = [ "url", ] +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "blocking", + "cfg-if", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version", + "spinning", + "thiserror", + "to_method", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2193,9 +2228,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -2367,9 +2402,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2407,7 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" dependencies = [ "cfg-if", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", ] @@ -2419,9 +2454,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" dependencies = [ "cfg-if", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -2520,6 +2555,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "ntest" version = "0.7.5" @@ -2543,7 +2587,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99a81eb400abc87063f829560bc5c5c835177703b83d1cd991960db0b2a00abe" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", ] @@ -2556,7 +2600,7 @@ checksum = "b10db009e117aca57cbfb70ac332348f9a89d09ff7204497c283c0f7a0c96323" dependencies = [ "ntest_proc_macro_helper", "proc-macro-crate", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", ] @@ -2626,9 +2670,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -2810,9 +2854,9 @@ checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -2823,15 +2867,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -2860,7 +2904,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "regex", "syn 1.0.109", @@ -2872,10 +2916,10 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "regex", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -3058,7 +3102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", "version_check", @@ -3070,7 +3114,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "version_check", ] @@ -3086,9 +3130,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] @@ -3170,7 +3214,7 @@ version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", ] [[package]] @@ -3394,7 +3438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2288c66aeafe3b2ed227c981f364f9968fa952ef0b30e84ada4486e7ee24d00a" dependencies = [ "cfg-if", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "rustc_version", "syn 1.0.109", @@ -3626,9 +3670,9 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -3710,9 +3754,9 @@ version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -3756,9 +3800,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling 0.20.9", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -3781,7 +3825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1b95bb2f4f624565e8fe8140c789af7e2082c0e0561b5a82a1b678baa9703dc" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "rustversion", "syn 1.0.109", @@ -3977,6 +4021,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + [[package]] name = "ssdp-client" version = "1.0.0" @@ -4029,7 +4082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "rustversion", "syn 1.0.109", @@ -4102,18 +4155,18 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.64" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "unicode-ident", ] @@ -4124,6 +4177,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys 0.8.6", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.52.0", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -4184,8 +4252,11 @@ dependencies = [ "base64 0.13.1", "clap 3.2.25", "crypto_box", + "daemonize", "dirs", + "futures", "hex", + "interprocess", "ipnetwork", "parking_lot", "rand", @@ -4196,6 +4267,7 @@ dependencies = [ "serde_json", "sha2", "shellwords", + "sysinfo", "telio", "telio-crypto", "telio-model", @@ -4740,22 +4812,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -4822,6 +4894,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.37.0" @@ -4847,9 +4925,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -4947,9 +5025,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -5143,7 +5221,7 @@ version = "0.3.1+v0.25.0" source = "git+https://github.com/NordSecurity/uniffi-rs.git?tag=v0.3.1+v0.25.0#1a5dc527165589456c3d7233107917e065192b03" dependencies = [ "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -5170,10 +5248,10 @@ dependencies = [ "camino", "fs-err", "once_cell", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "serde", - "syn 2.0.64", + "syn 2.0.65", "toml", "uniffi_build", "uniffi_meta", @@ -5339,7 +5417,7 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", "wasm-bindgen-shared", @@ -5373,7 +5451,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", "syn 1.0.109", "wasm-bindgen-backend", @@ -5489,6 +5567,25 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -5787,9 +5884,9 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] [[package]] @@ -5807,7 +5904,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.82", + "proc-macro2 1.0.83", "quote 1.0.36", - "syn 2.0.64", + "syn 2.0.65", ] diff --git a/clis/tcli/Cargo.toml b/clis/tcli/Cargo.toml index bcd665446..910b6e562 100644 --- a/clis/tcli/Cargo.toml +++ b/clis/tcli/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2018" license = "GPL-3.0-only" repository = "https://github.com/NordSecurity/libtelio" +default-run = "tcli" [dependencies] dirs = "4.0.0" @@ -15,6 +16,12 @@ reqwest = { version = "0.11.16", default-features = false, features = [ ] } rustyline = "11.0.0" shellwords = "1.1.0" +# Used only for checking if the daemon is running. +sysinfo = "0.30.11" +# Used as a lightweight and safe (because a TCP server has the risk of remote code execution) +# way for the API and daemon to communicate. +# Tokio support is needed, because the daemon runs on the async runtime. +interprocess = { version = "1.2.1" } anyhow.workspace = true base64.workspace = true @@ -22,6 +29,7 @@ clap.workspace = true crypto_box.workspace = true hex.workspace = true ipnetwork.workspace = true +futures.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tracing-appender.workspace = true @@ -45,3 +53,8 @@ telio-task.workspace = true telio-traversal.workspace = true telio-utils.workspace = true telio-wg.workspace = true + +[target.'cfg(unix)'.dependencies] +# Used to avoid the manual work of daemonizing the libtelio process. +# Also is the only reason why Windows isn't supported. +daemonize = "0.5.0" diff --git a/clis/tcli/contrib/example_vpn_disable_linux.sh b/clis/tcli/contrib/example_vpn_disable_linux.sh new file mode 100755 index 000000000..999d3296c --- /dev/null +++ b/clis/tcli/contrib/example_vpn_disable_linux.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# This is a simple example script on how to use TCLID to turn off VPN and clean up routing. + +# NOTE: This is meant to be run from the libtelio directory. +# And uses the debug build so that you could iterate quicker builds when debugging. + +TCLID_PATH="target/debug/tclid" + +# This script takes a single argument which is the name of the tunnel that Telio is using. +if [ $# -eq 0 ]; then + echo "No tunnel name provided, using tun10 as default." + TUNNEL_NAME="tun10" +else + TUNNEL_NAME=$1 +fi + +# Only Linux is supported at the moment. +if [[ $(uname) != "Linux" ]]; then + echo "This script is only compatible with Linux." + exit 126 +fi + +# Check if the script is being run as root to precent half run scripts. +if [ "$EUID" -ne 0 ]; then + echo "ERROR: This script must be run with root privileges." + exit 13 +fi + +if ! [ -f "$TCLID_PATH" ]; then + echo "Cannot find TCLID in $TCLID_PATH directory. Please build TCLID and run it from libtelio root." + exit 127 +fi + +# Check if the interface exists. +if ! ip link show | grep -q "$TUNNEL_NAME"; then + echo "Tunnel $TUNNEL_NAME does not exist. Enable VPN before disabling it again." + exit 1 +fi + +# Ensure ip is installed. +if ! command -v ip &> /dev/null; then + echo "iproute2 is required but not found. Please install it." + exit 127 +fi + +# Save current external IP to verif that it had changed later. +old_public_ip=$(curl -s ifconfig.me) +echo "External IP before enabling VPN: ${old_public_ip}" + +# Stop WireGuard. +eval ${TCLID_PATH} dev stop +# Stop libtelio daemon. +eval ${TCLID_PATH} quit + +# Delete VPN route. +# Routing into tunnel and routing from tunnel to VPN server will be deleted automatically when stopping Wireguard. +ip rule del not from all fwmark 11673110 lookup 73110 + +# Check if the public IP really changed. +new_public_ip=$(curl -s ifconfig.me) +if [ "$new_public_ip" == "$old_public_ip" ]; then + echo "Error: Public IP had not been changed!" + exit 1 +fi + +echo "Success: External IP after enabling VPN: ${new_public_ip}, you are no longer connected over a VPN." diff --git a/clis/tcli/contrib/example_vpn_enable_linux.sh b/clis/tcli/contrib/example_vpn_enable_linux.sh new file mode 100755 index 000000000..2a4eee105 --- /dev/null +++ b/clis/tcli/contrib/example_vpn_enable_linux.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# This is a simple example script on how to use TCLID to set up VPN. + +# NOTE: This is meant to be run from the libtelio directory. +# Also it assumes that your nord token is in the $NORD_TOKEN variable. So also make sure to run it with `sudo -E`. +# And uses the debug build so that you could iterate quicker builds when debugging. + +# This script takes a single argument which is the name of the tunnel for Telio to use. +if [ $# -eq 0 ]; then + echo "No tunnel name provided, using tun10 as default." + TUNNEL_NAME="tun10" +else + TUNNEL_NAME=$1 +fi + +TCLID_PATH="target/debug/tclid" + +# Below are a few checks to make sure that the environment is correct for this script to run and not mess up the network configuration: +# If it does mess up the network configuration, you can just restart your PC ;) + +# Only Linux is supported at the moment. +if [[ $(uname) != "Linux" ]]; then + echo "This script is only compatible with Linux." + exit 126 +fi + +# Ensure curl is installed. +if ! command -v curl &> /dev/null; then + echo "curl is required but not found. Please install it." + exit 127 +fi + +# Ensure jq is installed. +if ! command -v jq &> /dev/null; then + echo "jq is required but not found. Please install it." + exit 127 +fi + +# Ensure ip is installed. +if ! command -v ip &> /dev/null; then + echo "iproute2 is required but not found. Please install it." + exit 127 +fi + +if ! [ -f "$TCLID_PATH" ]; then + echo "Cannot find TCLID in $TCLID_PATH directory. Please build TCLID and run it from libtelio root." + exit 127 +fi + +# Function to validate NORD_TOKEN +validate_nord_token() { + local token="$1" + if [[ ! "$token" =~ ^[[:alnum:]]{64}$ ]]; then + echo "Error: Invalid NORD_TOKEN. It must be a 64-character-long alphanumeric string." + return 1 + fi +} + +# Check if the script is being run as root to precent half run scripts. +if [ "$EUID" -ne 0 ]; then + echo "ERROR: This script must be run with root privileges." + exit 13 +fi + +# Check if the NORD_TOKEN variable is set +if [ -z "$NORD_TOKEN" ]; then + echo "NORD_TOKEN variable is not set." + read -p "Please enter your NORD_TOKEN or run this script with \`sudo -E\`: " NORD_TOKEN +fi + +# Validate the entered NORD_TOKEN +if ! validate_nord_token "$NORD_TOKEN"; then + exit 1 +fi + +# Below is the actual libtelio usage: + +# Check if the interface already exists. +if ip link show | grep -q "$TUNNEL_NAME"; then + echo "Tunnel $TUNNEL_NAME already exists. Disable VPN before enabling it again." + exit 1 +fi + +# Save current external IP to verif that it had changed later. +old_public_ip=$(curl -s ifconfig.me) +echo "External IP before enabling VPN: ${old_public_ip}" + +# Starting WireGuard. +eval ${TCLID_PATH} dev start boringtun ${TUNNEL_NAME} + +# Getting VPN server IP and public key. +recommended_servers_list=$(curl -s "https://api.nordvpn.com/v1/servers/recommendations?&filters\[servers_technologies\]\[identifier\]=wireguard_udp&limit=1" -u token:$NORD_TOKEN) +recommended_server_public_key=$(echo ${recommended_servers_list} | jq --raw-output '.[].technologies[]|select(.identifier == "wireguard_udp")|.metadata[].value') +recommended_server_ip=$(echo ${recommended_servers_list} | jq --raw-output '.[].station') + +# Configuring interface. +ip addr add dev ${TUNNEL_NAME} 10.5.0.2/10 +ip link set up dev ${TUNNEL_NAME} +ip link set dev ${TUNNEL_NAME} mtu 1420 + +# Connecting to VPN. +eval ${TCLID_PATH} dev con ${recommended_server_public_key} ${recommended_server_ip}:51820 + +# Creating VPN route. +ip route add default dev ${TUNNEL_NAME} table 73110 +ip route add ${recommended_server_ip} dev ${TUNNEL_NAME} table 73110 +ip rule add not from all fwmark 11673110 lookup 73110 + +# Check if the public IP really changed. +new_public_ip=$(curl -s ifconfig.me) +if [ "$new_public_ip" == "$old_public_ip" ]; then + echo "Error: Public IP had not been changed!" + exit 1 +fi + +echo "Success: External IP after enabling VPN: ${new_public_ip}, you are now connected over a VPN." diff --git a/clis/tcli/src/main.rs b/clis/tcli/src/bin/tcli.rs similarity index 89% rename from clis/tcli/src/main.rs rename to clis/tcli/src/bin/tcli.rs index 7a863f354..7ab9890f6 100644 --- a/clis/tcli/src/main.rs +++ b/clis/tcli/src/bin/tcli.rs @@ -1,17 +1,18 @@ #![allow(unwrap_check)] -mod cli; -mod derp; -mod nord; +use std::{fs, io::Write, sync::Arc, time::SystemTime}; use anyhow::{anyhow, Result}; use clap::Parser; use dirs::home_dir; use parking_lot::Mutex; use regex::Regex; -use std::{io::Write, sync::Arc, time::SystemTime}; +use tracing::level_filters::LevelFilter; + use telio_model::{config::Server, event::Event as DevEvent, features::Features}; +use tcli::cli; + #[derive(Parser)] struct Args { /// Pass [Features] in json format. Configure optional features. @@ -24,6 +25,16 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); + let (non_blocking_writer, _tracing_worker_guard) = + tracing_appender::non_blocking(fs::File::create("tcli.log")?); + tracing_subscriber::fmt() + .with_max_level(LevelFilter::TRACE) + .with_writer(non_blocking_writer) + .with_ansi(false) + .with_line_number(true) + .with_level(true) + .init(); + let token = std::env::var("NORD_TOKEN").ok(); let features: Features = args diff --git a/clis/tcli/src/bin/tclid/coms.rs b/clis/tcli/src/bin/tclid/coms.rs new file mode 100644 index 000000000..44ae84cf5 --- /dev/null +++ b/clis/tcli/src/bin/tclid/coms.rs @@ -0,0 +1,142 @@ +//! Code for handling inter process communication and abstracting away it's dependencies. + +use std::{ + fs, + io::{BufRead, BufReader, Read, Write}, + path::Path, +}; + +use anyhow::Result; +use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; + +/// Struct for handling connections of the TCLID daemon's side of the IPC communication with the API. +pub struct DaemonSocket { + /// The inner socket over which the actual communication is happening. + socket: LocalSocketListener, +} + +impl DaemonSocket { + /// Binds the IPC socket to the specified address and returns a handle to the struct + /// containing the IPC socket. + /// + /// # Arguments + /// + /// * `ipc_socket_path` - Path of the IPC socket. + /// + /// # Returns + /// + /// An instance of Self wrapped inside of a Result. + pub fn new(ipc_socket_path: &Path) -> Result { + // Delete the socket file if it already exists + let _ = fs::remove_file(ipc_socket_path); + let socket = LocalSocketListener::bind(ipc_socket_path)?; + + Ok(Self { socket }) + } + + /// Opens a connection on the IPC socket for listening to commands (requests) from the API and + /// returns them to be processed by the event loop. + /// The same connection is used to send responses back to the API when the event loop produces them. + /// Note that since a single socket is used, care should be taken to not deadlock it. + /// + /// # Returns + /// + /// A struct for receiving commands from the new IPC connection stream and responding to them. + pub fn accept(&self) -> Result { + let stream = self.socket.accept()?; + + Ok(DaemonConnection { stream }) + } + + /// Function for sending commands to the server. + /// Also retrieves the response from the IPC server, but blocks while waiting for it. + /// Note that endlines are used to indicate the end of a message so the server knows when to stop reading and + /// send a response, otherwise the server would wait until the API closes the socket and won't be able to send a response. + /// This is also sort of a deadlock risk, so should be handled with care. + /// + /// # Arguments + /// + /// * `addr` - Address (more specifically path, but left address for clarity) of the IPC socket. + /// * `cmd` - Command to be sent to the daemon. + /// + /// # Returns + /// + /// A Result containing a response string. + pub fn send_command(addr: &Path, cmd: &str) -> Result { + let mut response_buffer = String::new(); + let mut stream = LocalSocketStream::connect(addr)?; + writeln!(stream, "{}", cmd)?; + stream.read_to_string(&mut response_buffer)?; + + Ok(response_buffer) + } + + /// Function Implementing custom retry logic for send_command, because the public retry crates I found seemed to + /// either have way too much overhead or be very sketchy in terms of quality. + /// + /// # Arguments + /// + /// * `addr` - Address (more specifically path, but left address for clarity) of the IPC socket (same as `send_command`). + /// * `cmd` - Command to be sent to the daemon (same as `send_command`). + /// * `retry_delay_list` - a list of retry delays. The length of the list is the retry count. + /// + /// # Returns + /// + /// A Result containing a response string (same as `send_command`). + pub fn send_command_with_retries( + addr: &Path, + cmd: &str, + retry_delay_list: &[tokio::time::Duration], + ) -> Result { + let mut retry_delay_list = retry_delay_list.iter().rev(); + + loop { + match Self::send_command(addr, cmd) { + Ok(response) => break Ok(response), + Err(e) => match retry_delay_list.next() { + Some(retry_delay) => std::thread::sleep(*retry_delay), + None => break Err(e), + }, + } + } + } +} + +/// Struct for tracking a single IPC connection and keeping the stream +/// Mostly needed for keeping the stream while the server is generating a reply. +pub struct DaemonConnection { + // The inner stream to be abstracted away. + stream: LocalSocketStream, +} + +impl DaemonConnection { + /// Function for reading from the IPC socket while also abstracting dependencies + /// and annoying implementation details like new line handling. + /// + /// # Returns + /// + /// String containing the received command. + pub fn read_command(&mut self) -> Result { + let mut command_buffer = String::new(); + let mut reader = BufReader::new(std::io::Read::by_ref(&mut self.stream)); + reader.read_line(&mut command_buffer)?; + // Removing the trailing newline used as a message ending according to platform. + #[cfg(unix)] + command_buffer.pop(); + #[cfg(not(unix))] + todo!("Windows support is not implemented yet"); + + Ok(command_buffer) + } + + /// Function for writing a reply back to the IPC socket after processing the received command. + /// + /// # Arguments + /// + /// * `response` - The response produced by handling a command. + pub fn respond(&mut self, response: String) -> Result<()> { + self.stream.write_all(response.as_bytes())?; + + Ok(()) + } +} diff --git a/clis/tcli/src/bin/tclid/daemon.rs b/clis/tcli/src/bin/tclid/daemon.rs new file mode 100644 index 000000000..cec50b0e1 --- /dev/null +++ b/clis/tcli/src/bin/tclid/daemon.rs @@ -0,0 +1,114 @@ +//! Code for general daemon handling utilities and abstracting away related dependencies. + +use std::path::PathBuf; + +use anyhow::{self, Result}; +use sysinfo::{ProcessRefreshKind, RefreshKind, System}; + +use crate::TCLID_WD_PATH; + +/// Enum for figuring out the current process after the daemon is forked away +/// from the API by the daemonization. +pub enum ProcessType { + /// Getting this value after the daemonization means that the current process is the API. + Api, + /// Getting this value after the daemonization means that the current process is the Daemon. + Daemon, +} + +/// Struct for packaging together the functions for managing the daemon. +/// Currently does not need any members, since most of the data is stored +/// in files anyway. +pub struct Daemon; + +impl Daemon { + /// Checks whether the daemon is currently running. + /// Won't work if pid file had been tampered with or it's location changed. + /// + /// # Returns + /// + /// Result containing a bool. True means that the daemon is running. + pub fn is_running() -> Result { + let system = System::new_with_specifics( + RefreshKind::new().with_processes(ProcessRefreshKind::everything()), + ); + + match std::fs::read_to_string(get_wd_path()?.join("tclid.pid")) { + Ok(pid_str) => { + let pid: usize = pid_str.trim().parse()?; + match system.process(sysinfo::Pid::from(pid)) { + Some(_) => Ok(true), + None => Ok(false), + } + } + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => Ok(false), + _ => Err(e.into()), + }, + } + } + + /// Function for conveniently printing the stderr file of the daemon. + /// Used by the API to quickly inform the user if something is wrong + /// with the daemon. + /// + /// # Returns + /// + /// The whole contents of the daemons stderr file as a String. + pub fn get_stderr() -> Result { + let stderr_file = get_wd_path()?.join("tclid.err"); + + let stderr_string = std::fs::read_to_string(stderr_file)?; + + Ok(stderr_string) + } + + /// Forks the daemon from the API process. + /// + /// # Returns + /// + /// Current process type indicating whether it's the API or the Daemon. + /// This is useful because of how the fork syscall. + pub fn start() -> Result { + #[cfg(unix)] + { + use std::fs::File; + // This crate only works on unix systems and not Windows. + use daemonize::Daemonize; + + let tclid_wd_path = get_wd_path()?; + + std::fs::create_dir_all(&tclid_wd_path)?; + let stderr = File::create(tclid_wd_path.join("tclid.err"))?; + + let daemonize = Daemonize::new() + .pid_file("tclid.pid") // Every method except `new` and `start` + .chown_pid_file(true) // is optional, see `Daemonize` documentation + .working_directory(tclid_wd_path) // for default behavior. + .stderr(stderr); // Redirect stderr to `/tmp/daemon.err`. + + match daemonize.execute() { + daemonize::Outcome::Parent(Ok(daemonize::Parent { .. })) => Ok(ProcessType::Api), + daemonize::Outcome::Parent(Err(err)) => Err(err.into()), + daemonize::Outcome::Child(Ok(_)) => Ok(ProcessType::Daemon), + daemonize::Outcome::Child(Err(err)) => Err(err.into()), + } + } + #[cfg(not(unix))] + todo!("Windows support not implemented for TCLID"); + } +} + +/// Private helper function for getting the path to the daemon's working directory. +/// +/// # Returns +/// +/// Result containing the path of the daemons work directory. +fn get_wd_path() -> Result { + Ok(std::env::current_exe()? + .parent() + .ok_or(anyhow::anyhow!( + "Cannot create daemon working directory at root" + ))? + .join(TCLID_WD_PATH)) +} diff --git a/clis/tcli/src/bin/tclid/doc/tclid_documentation.md b/clis/tcli/src/bin/tclid/doc/tclid_documentation.md new file mode 100644 index 000000000..511667665 --- /dev/null +++ b/clis/tcli/src/bin/tclid/doc/tclid_documentation.md @@ -0,0 +1,99 @@ +# TCLID + +A daemonized version of TCLI, which can be used in scripts. + +## How To Build TCLID + +Currently TCLID is implemented as a package of the libtelio project, just like TCLI, so it is build just like TCLI too: + +```shell +cargo build -p tcli --bin tclid +``` + +### Building The Documentation + +The full documentation for TCLID can be built with the following command: + +```shell +cargo doc -p tcli --bin tclid --open --no-deps --document-private-items +``` + +## How To Use TCLID + +The usage of TCLID is the same as TCLI with one main difference: **TCLID works in the background so there's no need for an STDIO interface** + +So instead of running a TCLI executable and then writing TCLI commands into the STDIO based CLI you write ` ` directly into your terminal **or from a script**. + +Here's an example of how to run meshnet using TCLID: + +```shell +./tclid mesh on tun10 +``` + +You can also use TCLID directly through cargo to build and run using only one command: + +```shell +cargo run -p tcli --bin tclid -- mesh on tun10 +``` + +See `example_vpn_enable_linux.sh` and `example_vpn_disable_linux.sh` for examples on how to use TCLID in a script. + +### How To Start TCLID Daemon + +The TCLID daemon is started when running any command (except for `quit` for obvious reasons), or even no commands so you don't really need to worry about starting it manually. + +So if you want to just start the TCLID daemon, run: + +```shell +./tclid +``` + +### How To Stop TCLID + +Because TCLID runs as a daemon in the background it needs to be stopped manually using the same `quit` command which is used by TCLI. + +So TCLID can be stopped by running: + +```shell +./tclid quit +``` + +>NOTE: If due to a bug the daemon fails to stop using the `quit` command, you can run `sudo cat /var/run/tclid_wd/tclid.pid | sudo xargs kill -9` from the root of the libtelio repo (or wherever the tclid.pid fie is visible from). + +### Feature Flags + +Feature flags are specified by putting the -f flag as a json string with no whitespaces: + +```shell +./target/debug/tclid -f '{"direct":{}}' +``` + +>NOTE: Feature flags can only be set when starting the daemon, so if it's already running, you'll need to stop it first. + +### Help + +Help command works the same way as with TCLI and will not start the TCLID daemon. + +So if you want to find out how to use the device connect command, run: + +```shell +./target/debug/tclid dev con --help +``` + +## Logs + +Logs along with other files related to TCLID can be found at `/var/run/tclid_wd/` directory. + +## Platform Support + +- Linux is supported and tested. +- MacOS is supported, but not tested. +- Windows is not supported yet. + +## Compatibility + +All of the examples given here were tested with: + +- rustc 1.77.2 (25ef9e3d8 2024-04-09) +- cargo 1.77.2 (e52e36006 2024-03-26) +- rustdoc 1.77.2 (25ef9e3d8 2024-04-09) diff --git a/clis/tcli/src/bin/tclid/main.rs b/clis/tcli/src/bin/tclid/main.rs new file mode 100644 index 000000000..913aed0a1 --- /dev/null +++ b/clis/tcli/src/bin/tclid/main.rs @@ -0,0 +1,191 @@ +#![deny(missing_docs)] +// TCLID doc rs documentation introduction: +#![doc = include_str!["./doc/tclid_documentation.md"]] + +//! Main and code for the API which is used for sending commands to libtelio which runs on a daemon. + +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use clap::Parser; +use tokio::time::Duration; + +mod coms; +mod daemon; +mod tclid; +mod telio_cli_interface; + +use coms::DaemonSocket; +use daemon::{Daemon, ProcessType}; +use telio_cli_interface::TelioCliInterface; + +/// Name for the local socket file which will be used for inter process communication. +const DAEMON_IPC_SOCKET: &str = "tclid.sock"; +/// Path where all the logs, local socket and pid file will be stored, relative to the path of this binary. +const TCLID_WD_PATH: &str = "/var/run/tclid_wd"; + +// TODO Add meshnet examples. +// TODO Add --restart flag which would restart the whole daemon before executing the provided command for faster iterating when debugging. +// TODO figure out logging, because multiple tracing subscribers can't be used. Maybe reroute STDIO of daemon? +// TODO Maybe add auto completion support? (probably won't work due to mixed parsing implementation) + +/// Argument structure for the TCLID API used by Clap to parse the arguments. +#[derive(Parser)] +#[clap(disable_help_flag = true)] +struct Args { + /// This flag is set when --help or -h is passed, but help is disabled for this struct, + /// because we want to retrieve the help message from the original TCLI parser to keep things simple. + /// So help is handled separately according to this flag. + #[clap(short = 'h', long = "help")] + help: bool, + /// Used to pass in features to the TCLID daemon. Note that the TCLID daemon only accepts + /// feature flags on startup. + #[clap(short, long)] + features: Option, + /// Holds all the other arguments which will be forwarded to the TCLI parser. + input: Vec, +} + +/// Main of the whole TCLID package. +fn main() -> Result<()> { + let args: Args = Args::parse(); + + match run_api(args) { + Ok(()) => Ok(()), + Err(e) => { + #[cfg(unix)] + // For convenience printing stderr of the daemon to inform the user that something's wrong. + if let Ok(stderr) = Daemon::get_stderr() { + if !stderr.is_empty() { + println!("Daemon stderr: {}", stderr); + } + } + Err(e) + } + } +} + +/// Helper function for putting together the directory in which the TCLID socket file will reside. +/// +/// # Returns +/// +/// Path to the IPC socket. +fn get_ipc_socket_path() -> Result { + // Using current_exe() so that if the path was relative, the path configuration would still work. + Ok(std::env::current_dir()? + .parent() + .ok_or(anyhow!("Can't create working directory at root"))? + .join(TCLID_WD_PATH) + .join(DAEMON_IPC_SOCKET)) +} + +/// Separate function that handles printing the help message, +/// because we don't want to start the whole TCLID daemon just for that. +/// +/// # Arguments +/// +/// * `input` - The command that was sent through the API when requesting for help. +fn print_help(mut input: Vec) -> Result<()> { + // Clap removes the actual flag from the args, so need to bring it back, since we're forwarding this to the Telio parser. + input.push("--help".to_owned()); + // CLI also needs the first element of the vector to be the application name. + input.insert(0, "tclid".to_owned()); + + println!("{}", TelioCliInterface::get_help(input)?); + + Ok(()) +} + +/// Main function of the API. This is where all the API's decision making happens. +/// +/// # Arguments +/// +/// * `args` - Command line arguments for the API (already parsed). +fn run_api(args: Args) -> Result<()> { + // We don't want to start the daemon if all we want is to print the help message. + // But we also have to retrieve the help message from the TCLI parser. + if args.help { + return print_help(args.input); + } + + match args.input.first().map(|s| s.as_str()) { + // If no arguments are supplied default behavior is starting the daemon. + None => { + if Daemon::is_running()? { + eprintln!("TCLID daemon is already running, stop it by calling `tclid quit`"); + } else { + start_daemon(args.features.clone())?; + } + } + Some("quit") => { + if Daemon::is_running()? { + let response = DaemonSocket::send_command(&get_ipc_socket_path()?, "quit")?; + + if response.as_str() == "OK" { + println!("TCLID daemon stopped"); + } else { + eprintln!("Error while trying to stop TCLID daemon, please check if TCLID daemon is still running and close it manually"); + } + } else { + eprintln!("TCLID daemon is not running"); + } + } + Some(_) => { + if !Daemon::is_running()? { + start_daemon(args.features.clone())?; + } else if args.features.is_some() { + eprintln!("Features cannot be set while TCLID daemon is running."); + } + let response = + DaemonSocket::send_command(&get_ipc_socket_path()?, &args.input.join(" "))?; + println!("{}", response); + } + }; + + Ok(()) +} + +/// Starts the TCLID daemon which will run actual Telio. For Unix systems uses forking. Decided not to +/// separate api and tclid.d into separate binaries in the project, because that added a lot of extra work. +/// Like having to build the tclid.d binary manually before running the api and didn't really pay off the extra effort. +/// +/// # Arguments +/// +/// * `features` - Optional string containing the features specification for Telio in JSON. +fn start_daemon(features: Option) -> Result<()> { + println!("Starting TCLID daemon..."); + match Daemon::start() { + Ok(ProcessType::Api) => { + // Verifying that the server had been started, but we have no way of synchronizing + // the verification with the daemon, so using retries instead. + // The delay values had been chose experimentally to get a fast, but efficient start of the daemon. + // This delay mostly depends on how quickly Telio initializes itself from inside the daemon. + DaemonSocket::send_command_with_retries( + &get_ipc_socket_path()?, + "status", + &[ + Duration::from_millis(400), + Duration::from_millis(100), + Duration::from_millis(100), + Duration::from_millis(100), + Duration::from_millis(500), + ], + )?; + if let Some(features) = features { + println!("TCLID daemon started with features: {}", features); + } else { + println!("TCLID daemon started"); + } + + Ok(()) + } + Ok(ProcessType::Daemon) => match tclid::run_tclid(features) { + Ok(()) => Ok(()), + Err(e) => { + eprintln!("{e}"); + Err(e) + } + }, + Err(e) => Err(e), + } +} diff --git a/clis/tcli/src/bin/tclid/tclid.rs b/clis/tcli/src/bin/tclid/tclid.rs new file mode 100644 index 000000000..a151adf24 --- /dev/null +++ b/clis/tcli/src/bin/tclid/tclid.rs @@ -0,0 +1,58 @@ +//! Code for the daemon which runs actual libtelio in the background. + +use std::fs; + +use anyhow::Result; +use tracing::{info, level_filters::LevelFilter}; + +use crate::coms::DaemonSocket; +use crate::get_ipc_socket_path; +use crate::telio_cli_interface::TelioCliInterface; + +/// Function to setup the TCLID daemon's dependencies (including Telio itself) and and run it. +/// AKA the event loop. +/// +/// # Arguments +/// +/// * `features` - Optional string containing the features specification for Telio in JSON. +/// +/// # Returns +/// +/// A Result indicating whether the event loop shut down gracefully. +pub fn run_tclid(features: Option) -> Result<()> { + let (non_blocking_writer, _tracing_worker_guard) = + tracing_appender::non_blocking(fs::File::create("tclid.log")?); + tracing_subscriber::fmt() + .with_max_level(LevelFilter::TRACE) + .with_writer(non_blocking_writer) + .with_ansi(false) + .with_line_number(true) + .with_level(true) + .init(); + + let nord_token = std::env::var("NORD_TOKEN").ok(); + let mut telio_cli = TelioCliInterface::new(nord_token, features)?; + let socket = DaemonSocket::new(&get_ipc_socket_path()?)?; + info!("Entered event loop"); + + loop { + let mut connection = socket.accept()?; + let command = connection.read_command()?; + + match command.as_str() { + "quit" => { + // "OK" here is an arbitrary response to indicate that the server had started shutting down. + connection.respond("OK".to_owned())?; + + break; + } + _ => { + let response = telio_cli.exec(command); + connection.respond(response?)?; + } + } + } + info!("Stopping daemon"); + + Ok(()) +} diff --git a/clis/tcli/src/bin/tclid/telio_cli_interface.rs b/clis/tcli/src/bin/tclid/telio_cli_interface.rs new file mode 100644 index 000000000..9bf828895 --- /dev/null +++ b/clis/tcli/src/bin/tclid/telio_cli_interface.rs @@ -0,0 +1,78 @@ +//! Code for handling and communicating with libtelio and abstracting it away as a dependency. + +use std::sync::Arc; + +use anyhow::Result; + +use telio_model::features::Features; + +use tcli::cli; + +/// Platform specific line ending. +#[cfg(windows)] +const LINE_ENDING: &str = "\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &str = "\n"; + +/// Struct containing the CLI instance which is used to communicate with libtelio. +/// This instance is basically the TCLI command parser and is intentionally left unchanged to +/// make TCLID as simple to adapt to as possible for the developers who are used to TCLI. +/// +/// NOTE! The inner cli type must be dropped outside of the Tokio async runtime, so this struct must +/// be dropped outside of it as well! +pub struct TelioCliInterface { + /// Internal CLI instance. Same as used by TCLI. Panics if dropped inside the Tokio async runtime! + cli: cli::Cli, +} + +impl TelioCliInterface { + /// Starts a new instance of the libtelio CLI and it's dependencies, essentially making Telio ready + /// to receive commands. The dependencies include logging in with the provided Nord token, parsing + /// the feature flags into a readable format and starting a connection to the DERP server. + /// Note that because of how nord.rs is written, Telio login must happen outside of async context, + /// so this function must also be called outside of async context or it will panic. + /// Returns itself as the handle to the libtelio CLI instance. + /// + /// # Arguments + /// + /// * `token` - Nord token used to log into the Nord servers. + /// * `features` - Optional string containing the features specification for Telio in JSON. + pub fn new(token: Option, features: Option) -> Result { + let features: Features = features + .map(|s| serde_json::from_str(&s)) + .transpose()? + .unwrap_or_default(); + + let derp_server = + Arc::new(parking_lot::Mutex::>::new(None)); + + let cli = cli::Cli::new(features, token, derp_server)?; + + Ok(TelioCliInterface { cli }) + } + /// Execute the actual commands for libtelio received from the event loop of the daemon and return a response + /// so that it could be sent back to the API. Also handles the format matching between libtelio and TCLID. + /// + /// # Arguments + /// + /// * `cmd` - Command that was sent by the user through the API. + pub fn exec(&mut self, cmd: String) -> Result { + let resp = self.cli.exec(&cmd); + let resp = resp + .iter() + .map(|resp| resp.to_string()) + .collect::>() + .join(LINE_ENDING); + + Ok(resp) + } + /// Special helper function to retrieve the help message from the TCLI parser without the overhead of + /// having to start any libtelio dependencies or even run the daemon itself. + /// + /// # Arguments + /// + /// * `args` - Command line arguments forming the command for which the help is being requested. + pub fn get_help(args: Vec) -> Result { + cli::Cli::print_help(args) + } +} diff --git a/clis/tcli/src/cli.rs b/clis/tcli/src/cli.rs index d7c4f6d85..0b68af333 100644 --- a/clis/tcli/src/cli.rs +++ b/clis/tcli/src/cli.rs @@ -17,12 +17,10 @@ use tokio::{ time::{sleep, Duration}, }; use tracing::error; -use tracing::level_filters::LevelFilter; -use tracing_appender::non_blocking::WorkerGuard; use crate::nord::{Error as NordError, Nord, OAuth}; -use std::fs::File; +use std::fmt::Display; use std::str::FromStr; use std::time::SystemTime; use std::{ @@ -114,9 +112,9 @@ pub struct Cli { meshmap: Option, derp_client: DerpClient, derp_server: Arc>>, - _tracing_worker_guard: WorkerGuard, } +#[derive(Debug)] pub enum Resp { Info(String), Event { @@ -127,6 +125,25 @@ pub enum Resp { Quit, } +impl Display for Resp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Resp::Info(msg) => { + write!(f, "INFO: {}", msg) + } + Resp::Error(e) => { + write!(f, "ERROR: {:?}", e) + } + Resp::Quit => { + write!(f, "QUIT") + } + Resp::Event { ts, event } => { + write!(f, "EVENT: {:?}:{:?}", ts, event) + } + } + } +} + #[derive(Deserialize, Serialize, Clone)] struct MeshConf { name: String, @@ -337,16 +354,6 @@ impl Cli { token: Option, derp_server: Arc>>, ) -> anyhow::Result { - let (non_blocking_writer, _tracing_worker_guard) = - tracing_appender::non_blocking(File::create("tcli.log")?); - tracing_subscriber::fmt() - .with_max_level(LevelFilter::TRACE) - .with_writer(non_blocking_writer) - .with_ansi(false) - .with_line_number(true) - .with_level(true) - .init(); - let (sender, resp) = mpsc::channel(); let derp_server_lambda = derp_server.clone(); @@ -395,26 +402,28 @@ impl Cli { meshmap: None, derp_client: DerpClient::new(), derp_server, - _tracing_worker_guard, }) } + /// Function for only handling the help message without extra overhead. Used by the TCLID API. + pub fn print_help(args: Vec) -> anyhow::Result { + match Cmd::try_parse_from(args) { + Err(e) => anyhow::Result::Ok(e.to_string()), + _ => Err(anyhow::anyhow!("This is not a help command")), + } + } + pub fn exec(&mut self, cmd: &str) -> Vec { let mut res = Vec::new(); let mut args = cli_try![shellwords::split(cmd)]; args.insert(0, "tcli".to_owned()); let parse_try_result = Cmd::try_parse_from(&args); - match parse_try_result { Ok(cmd) => cli_res!(res; (j self.exec_cmd(cmd))), - Err(err) => match err.kind() { - clap::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand - | clap::ErrorKind::DisplayHelp => { - cli_res!(res; (i "{}", err)); - } - e => cli_res!(res; (e Error::Parser(e))), - }, + Err(err) => { + cli_res!(res; (i "{}", err)); + } } res } @@ -665,29 +674,25 @@ impl Cli { Ok(udp_socket) => { let udp_socket = Arc::new(udp_socket); - // Timeout for stunner - let sleep = sleep(Duration::from_secs(20)); - tokio::pin!(sleep); - // let socket = udp_socket.clone(); - const MAX_PACKET: usize = 65536; - let mut rx_buff = [0u8; MAX_PACKET]; - loop { - tokio::select! { - Ok((_len, _src_addr)) = udp_socket.recv_from(&mut rx_buff) => { - match rx_buff.first().map(|b| PacketTypeRelayed::from(*b)) { - Some(PacketTypeRelayed::Pinger) => cli_res!(res; (i "Pinger message received")), - Some(PacketTypeRelayed::Ponger) => cli_res!(res; (i "Ponger message received")), - other => cli_res!(res; (i "Unexpected packet: {:?}", other)), - } - break; - }, - () = &mut sleep => { - cli_res!(res; (i "timeout")); - break; - }, - - } - } + // Timeout for stunner + let sleep = sleep(Duration::from_secs(20)); + tokio::pin!(sleep); + // let socket = udp_socket.clone(); + const MAX_PACKET: usize = 65536; + let mut rx_buff = [0u8; MAX_PACKET]; + tokio::select! { + Ok((_len, _src_addr)) = udp_socket.recv_from(&mut rx_buff) => { + match rx_buff.first().map(|b| PacketTypeRelayed::from(*b)) { + Some(PacketTypeRelayed::Pinger) => cli_res!(res; (i "Pinger message received")), + Some(PacketTypeRelayed::Ponger) => cli_res!(res; (i "Ponger message received")), + other => cli_res!(res; (i "Unexpected packet: {:?}", other)), + } + }, + () = &mut sleep => { + cli_res!(res; (i "timeout")); + }, + + } } Err(_) => { cli_res!(res; (i "udp socket init error")); @@ -756,12 +761,12 @@ impl Cli { stun_port, )) { Ok(data) => { - cli_res!(res; (i"Public Address: {:?}", data.public_ip)); - cli_res!(res; (i"Nat Type: {:?}", data.nat_type)); + cli_res!(res; (i "Public Address: {:?}", data.public_ip)); + cli_res!(res; (i "Nat Type: {:?}", data.nat_type)); } Err(error) => { - cli_res!(res; (i"problem: {}", error)); + cli_res!(res; (i "problem: {}", error)); } }, } @@ -823,7 +828,10 @@ impl Cli { }) }; - let ip: IpNetwork = ip_address.parse().unwrap(); + let ip: IpNetwork = match ip_address.parse() { + Ok(ip) => ip, + Err(_) => return Err(Error::SettingIpFailed), + }; if std::env::consts::OS == "windows" { let mut args = vec![ diff --git a/clis/tcli/src/lib.rs b/clis/tcli/src/lib.rs new file mode 100644 index 000000000..13d42ddc0 --- /dev/null +++ b/clis/tcli/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cli; +mod derp; +mod nord; diff --git a/clis/tcli/src/nord.rs b/clis/tcli/src/nord.rs index b639fc28a..a27f10f33 100644 --- a/clis/tcli/src/nord.rs +++ b/clis/tcli/src/nord.rs @@ -159,7 +159,6 @@ impl Nord { let login = LoginInfo { token: token.to_string(), }; - let client = Client::new(); let creds: Creds = client .get(&format!("{}/users/services/credentials", API_BASE)) diff --git a/test_runner.sh b/test_runner.sh index 8c4235f3d..005d24ccd 100755 --- a/test_runner.sh +++ b/test_runner.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if command -v sudo &> /dev/null; then - sudo $@ + sudo -E $@ else $@ fi