commit 8f3bf5c7c7109821d737a6a67a7fd51fdf3b0917 Author: Yujia Qiao Date: Sat Dec 11 20:30:42 2021 +0800 feat: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40082fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +perf.data +perf.data.old diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5c3d5e8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,932 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backoff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe17f59a06fe8b87a6fc8bf53bb70b3aba76d7685f432487a68cd5552853625" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project", + "rand", + "tokio", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "clap" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicase", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdlimit" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4c9e43643f5a3be4ca5b67d26b98031ff9db6806c3440ae32e02e3ceac3f1b" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-core" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "os_str_bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rathole" +version = "0.1.0" +dependencies = [ + "anyhow", + "backoff", + "bincode", + "bytes", + "clap", + "fdlimit", + "hex", + "lazy_static", + "rand", + "ring", + "serde", + "socket2", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30e4c09749c107e83dd61baf9604198efc4542863c88af39dafcaca89c7c9f9" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f5a9e10 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rathole" +version = "0.1.0" +edition = "2021" +authors = ["Yujia Qiao "] +description = "A reverse proxy for NAT traversal" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.bench] +debug = 1 + +[dependencies] +tokio = { version = "1", features = ["full"] } +bytes = { version = "1"} +clap = "3.0.0-beta.5" +toml = "0.5" +serde = {version = "1.0", features = ["derive"]} +anyhow = "1.0" +ring = "0.16" +bincode = "1" +lazy_static = "1.4.0" +hex = "0.4" +rand = "0.8.0" +backoff = {version="0.3.0", features=["tokio"]} +tracing = "0.1" +tracing-subscriber = "0.2" +socket2 = "0.4" +fdlimit = "0.2.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..312c698 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# rathole +![rathole-logo](./doc/img/rathole-logo.png) + +A fast and stable reverse proxy for NAT traversal, written in Rust + +rathole, like frp, can help to expose the service on the device behind the NAT to the Internet, via a server with a public IP. + +## Quickstart + +To use rathole, you need a server with a public IP, and a device behind the NAT, where some services that need to be exposed to the Internet. + +Assuming you have a NAS at home behind the NAT, and want to expose its ssh service to the Internet: + +1. On the server which has a public IP + +Create `server.toml` with the following content and accommodate it to your needs. +```toml +# server.toml +[server] +bind_addr = "0.0.0.0:2333" # `2333` specifys the port that rathole listens for clients + +[server.services.my_nas_ssh] +token = "use_a_secret_that_only_you_know" # Token that is used to authenticate the client for the service. Change to a arbitrary value. +bind_addr = "0.0.0.0:5202" # `5202` specifys the port that exposes `my_nas_ssh` to the Internet +``` + +Then run: +```bash +./rathole server.toml +``` + +2. On the host which is behind the NAT (your NAS) + +Create `client.toml` with the following content and accommodate it to your needs. +```toml +[client] +remote_addr = "myserver.com:2333" # The address of the server. The port must be the same with the port in `server.bind_addr` + +[client.services.my_nas_ssh] +token = "use_a_secret_that_only_you_know" # Must be the same with the server to pass the validataion +local_addr = "127.0.0.1:22" # The address of the service that needs to be forwarded +``` + +Then run: +```bash +./rathole client.toml +``` + +3. Now the client will try to connect to the server `myserver.com` on port `2333`, and any traffic to `myserver.com:5202` will be forwarded to the client's port `22`. + +So you can `ssh myserver.com:5202` to ssh to your NAS. + +## Configuration +`rathole` can automatically determine to run in the server mode or the client mode, according to the content of the configuration file, if only one of `[server]` and `[client]` block is present, like the example in [Quickstart](#Quickstart). + +But the `[client]` and `[server]` block can also be put in one file. Then on the server side, run `rathole --server config.toml` and on the client side, run `rathole --client config.toml` to explictly tell `rathole` the running mode. + +Here is the full configuration specification: +```toml +[client] +remote_addr = "example.com:2333" # Necessary. The address of the server +default_token = "default_token_if_not_specify" # Optional. The default token of services, if they don't define their own ones + +[client.services.service1] # A service that needs forwarding. The name `service1` can change arbitrarily, as long as identical to the name in the server's configuration +token = "whatever" # Necessary if `client.default_token` not set +local_addr = "127.0.0.1:1081" # Necessary. The address of the service that needs to be forwarded + +[client.services.service2] # Multiple services can be defined +local_addr = "127.0.0.1:1082" + +[server] +bind_addr = "0.0.0.0:2333" # Necessary. The address that the server listens for clients. Generally only the port needs to be change. +default_token = "default_token_if_not_specify" # Optional + +[server.services.service1] # The service name must be identical to the client side +token = "whatever" # Necesary if `server.default_token` not set +bind_addr = "0.0.0.0:8081" # Necessary. The address of the service is exposed at. Generally only the port needs to be change. + +[server.services.service2] +bind_addr = "0.0.0.1:8082" +``` + +# Benchmark + +rathole has similiar latency to frp, but can handle more connections. Also it can provide much better bandwidth than frp. + +See also [Benchmark](./doc/benchmark.md). + +![tcp_bitrate](./doc/img/tcp_bitrate.svg) + +![tcp_latency](./doc/img/tcp_latency.svg) + +# Development + +`rathole` is in active development. A load of features is on the way: + +- [ ] UDP support +- [ ] TLS transport +- [ ] Hot reloading +- [ ] HTTP APIs for configuration diff --git a/doc/benchmark.md b/doc/benchmark.md new file mode 100644 index 0000000..71816a1 --- /dev/null +++ b/doc/benchmark.md @@ -0,0 +1,87 @@ +# Benchmark + +> Date: 2021/12/14 +> +> Arch Linux with 5.15.7-arch1-1 kernel +> +> Intel i7-6600U CPU @ 2.60GHz +> +> 20GB RAM + + +## Bitrate + +![tcp_bitrate](./img/tcp_bitrate.svg) + +rathole with the following configuration: +```toml +[client] +remote_addr = "localhost:2333" +default_token = "123" + +[client.services.foo1] +local_addr = "127.0.0.1:80" + +[server] +bind_addr = "0.0.0.0:2333" +default_token = "123" + +[server.services.foo1] +bind_addr = "0.0.0.0:5202" +``` + +frp 0.38.0 with the following configuration: +```ini +[common] +bind_port = 7000 +authentication_method = token +token = 1233 +``` +```ini +# frpc.ini +[common] +server_addr = 127.0.0.1 +#server_addr = 47.100.208.60 +server_port = 7000 +authentication_method = token +token = 1233 + +[ssh] +type = tcp +local_ip = 127.0.0.1 +local_port = 80 +remote_port = 5203 +``` + +``` +$ iperf3 -v +iperf 3.10.1 (cJSON 1.7.13) +Linux sig 5.15.7-arch1-1 #1 SMP PREEMPT Wed, 08 Dec 2021 14:33:16 +0000 x86_64 +Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, sendfile / zerocopy, socket pacing, authentication, bind to device, support IPv4 don't fragment +$ sudo iperf3 -s -p 80 +``` + +For rathole benchmark: +``` +$ iperf3 -c 127.0.0.1 -p 5202 +``` + +For frp benchmark: +``` +$ iperf3 -c 127.0.0.1 -p 5203 +``` + +## Latency + +nginx/1.20.2 listens on port 80, with the default test page. + +frp and rathole configuration is same with the previous section. + +Using [ali](https://github.com/nakabonne/ali) with different rate. + +e.g. for rathole 10 QPS benchmark: +``` +ali -r 10 http://127.0.0.1:5202 +``` + +![tcp_latency](./img/tcp_latency.svg) diff --git a/doc/img/overview.excalidraw b/doc/img/overview.excalidraw new file mode 100644 index 0000000..e1598eb --- /dev/null +++ b/doc/img/overview.excalidraw @@ -0,0 +1,1356 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "rectangle", + "version": 127, + "versionNonce": 80643966, + "isDeleted": false, + "id": "_ROJe0KCjbnKQjLDcc-Ag", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 274.66668701171875, + "y": 87.49995422363281, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 450.66668701171875, + "height": 208.66667175292972, + "seed": 1939336259, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [ + "1Sorez2zxxKqyRilx21-m", + "uJx77oj5eyZPw61wszaJN" + ], + "updated": 1639393963541 + }, + { + "type": "text", + "version": 248, + "versionNonce": 1524512610, + "isDeleted": false, + "id": "X-BwNQGYSBy-tPINiKBCt", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 283.33331298828125, + "y": 94.50007629394531, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 61, + "height": 25, + "seed": 429932333, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [], + "updated": 1639393963541, + "fontSize": 20, + "fontFamily": 1, + "text": "Server", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "type": "ellipse", + "version": 166, + "versionNonce": 1926031294, + "isDeleted": false, + "id": "5KLQ8EXnY3KjzuLRGbhJU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 873.3333129882812, + "y": 151.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 37.33331298828125, + "height": 34.66667175292969, + "seed": 565619875, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [ + "-lU_z4mfDB58ZiJ8HlTxY" + ], + "updated": 1639393963541 + }, + { + "type": "line", + "version": 112, + "versionNonce": 447019810, + "isDeleted": false, + "id": "ViC_qO7r1ED1cN1IlPe7s", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 892.6666259765625, + "y": 188.16668701171875, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 0, + "height": 34, + "seed": 1032403459, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963541, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ] + }, + { + "type": "line", + "version": 86, + "versionNonce": 177553406, + "isDeleted": false, + "id": "8SCEaNme89qCxY-xAS0it", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 890, + "y": 199.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 24, + "height": 18.66668701171875, + "seed": 773580109, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963541, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -24, + 18.66668701171875 + ] + ] + }, + { + "type": "line", + "version": 110, + "versionNonce": 73221858, + "isDeleted": false, + "id": "Kbl62J0jyfWlbTEVgMqJH", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 895.3333129882812, + "y": 197.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 24, + "height": 18, + "seed": 464452045, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963541, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 24, + 18 + ] + ] + }, + { + "type": "line", + "version": 130, + "versionNonce": 1881706558, + "isDeleted": false, + "id": "PRHAdurETJSYCaa5l6Iwa", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 892, + "y": 222.16668701171875, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 14.66668701171875, + "height": 25.33331298828125, + "seed": 1595489411, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963542, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -14.66668701171875, + 25.33331298828125 + ] + ] + }, + { + "type": "line", + "version": 162, + "versionNonce": 1888885410, + "isDeleted": false, + "id": "HkWBRTjPa-LTKYPbw8XS-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 894, + "y": 223.5, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 17.630663207545922, + "height": 23.561635782942176, + "seed": 1412110733, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963542, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 17.630663207545922, + 23.561635782942176 + ] + ] + }, + { + "type": "rectangle", + "version": 307, + "versionNonce": 1975983586, + "isDeleted": false, + "id": "2GauISsAXxBxXaARLdO2v", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 277.6666564941406, + "y": 419.99999237060547, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 442, + "height": 132.0000152587891, + "seed": 1008142253, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [], + "updated": 1639394067925 + }, + { + "type": "text", + "version": 375, + "versionNonce": 69970238, + "isDeleted": false, + "id": "4t6IqDCz_2ovUHEWf3VyP", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 287.16668701171875, + "y": 428.4999084472656, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 54, + "height": 25, + "seed": 1136307299, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [ + "uJx77oj5eyZPw61wszaJN" + ], + "updated": 1639394067925, + "fontSize": 20, + "fontFamily": 1, + "text": "Client", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "type": "text", + "version": 406, + "versionNonce": 1298876478, + "isDeleted": false, + "id": "i-iOOSRyBhiISzIY5AG2O", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 650, + "y": 135.50001525878906, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 72, + "height": 40, + "seed": 1004543373, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [ + "1Sorez2zxxKqyRilx21-m" + ], + "updated": 1639394028864, + "fontSize": 16, + "fontFamily": 1, + "text": "service1\nbind addr", + "baseline": 34, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "type": "text", + "version": 471, + "versionNonce": 437130622, + "isDeleted": false, + "id": "Lld8m5f8AeGoMRmkfryGK", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 650, + "y": 246.1667022705078, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 72, + "height": 40, + "seed": 1760597182, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [ + "1Sorez2zxxKqyRilx21-m" + ], + "updated": 1639394090709, + "fontSize": 16, + "fontFamily": 1, + "text": "service2\nbind addr", + "baseline": 34, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "type": "text", + "version": 212, + "versionNonce": 1840891362, + "isDeleted": false, + "id": "5io-dv6h3U5ORt9DXFq37", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 308.66668701171875, + "y": 250.83334350585938, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 72, + "height": 40, + "seed": 1771953379, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [ + "wLQ-nby5mNnwfX9LnFrEt" + ], + "updated": 1639393963542, + "fontSize": 16, + "fontFamily": 1, + "text": "server\nbind addr", + "baseline": 34, + "textAlign": "center", + "verticalAlign": "top" + }, + { + "type": "arrow", + "version": 228, + "versionNonce": 1728857058, + "isDeleted": false, + "id": "-lU_z4mfDB58ZiJ8HlTxY", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 870.7065228655306, + "y": 194.87651239705974, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 129.96929122242796, + "height": 1.4508687081649327, + "seed": 585847683, + "groupIds": [], + "strokeSharpness": "round", + "boundElementIds": [], + "updated": 1639393963543, + "startBinding": { + "elementId": "5KLQ8EXnY3KjzuLRGbhJU", + "focus": -1.5107931785090518, + "gap": 15.791401051287782 + }, + "endBinding": { + "elementId": "mR2qjxJFdOso9NGgCoq4h", + "focus": 0.3208591338543321, + "gap": 10.737231643102632 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -129.96929122242796, + -1.4508687081649327 + ] + ] + }, + { + "id": "2DQbzxVigt_dM1muvXWYN", + "type": "text", + "x": 872.8333740234375, + "y": 127, + "width": 49, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1110579390, + "version": 21, + "versionNonce": 974179198, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393963543, + "text": "visitor", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 14 + }, + { + "id": "LU4D6A2Ugd1V9uKE6SwOC", + "type": "arrow", + "x": 696.2380319060499, + "y": 189.44727288880773, + "width": 134.59847736695497, + "height": 6.61671213194353, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 275565346, + "version": 351, + "versionNonce": 481916706, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393963543, + "points": [ + [ + 0, + 0 + ], + [ + -134.59847736695497, + 6.61671213194353 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "mR2qjxJFdOso9NGgCoq4h", + "focus": 0.0597242207313621, + "gap": 9.095342117387645 + }, + "endBinding": { + "elementId": "NzpaVP1cgsvfg6KfdD99G", + "focus": 0.1507396149689704, + "gap": 3.4879322512409843 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "NzpaVP1cgsvfg6KfdD99G", + "type": "diamond", + "x": 372.66668701171875, + "y": 152.5, + "width": 184, + "height": 84, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 891739170, + "version": 42, + "versionNonce": 1136678334, + "isDeleted": false, + "boundElementIds": [ + "LU4D6A2Ugd1V9uKE6SwOC", + "xm8fFB4fOVowURVtFEyfx", + "E-k0fg9CKUsCbBcIFgpQN", + "iqQRk3oncpFlTohh4RxWf", + "S1o9eYMClf4Mrmfw9HlDs", + "nbIlU5kICCXoOhMWP1aoq", + "4mPQElLVeuU0MBB9zyNTL" + ], + "updated": 1639394052440 + }, + { + "id": "6Ym2F9bT0rNpkiLbkm6Ku", + "type": "text", + "x": 414.6666564941406, + "y": 185.50003051757812, + "width": 112, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 162216574, + "version": 33, + "versionNonce": 199316542, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393963543, + "text": "rathole server", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 14 + }, + { + "id": "72LJc8JYfizCW-59n-YiJ", + "type": "diamond", + "x": 313.9999694824219, + "y": 456.83331298828125, + "width": 172.66668701171875, + "height": 74.66668701171876, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 627082402, + "version": 203, + "versionNonce": 1475968062, + "isDeleted": false, + "boundElementIds": [ + "xm8fFB4fOVowURVtFEyfx", + "CYPbqJ97T4dK8aTY2NoA6", + "xQjRQnu2M-Lx4L_FApAWi", + "ZBwjcWgJYIRx-XieGSul2", + "DjwSuFQtjGNkkF4rl7myd", + "8qillKpd5VKO0hrasQMVX" + ], + "updated": 1639394068076 + }, + { + "id": "mR2qjxJFdOso9NGgCoq4h", + "type": "rectangle", + "x": 705.3333740234375, + "y": 176.83335876464844, + "width": 24.6666259765625, + "height": 24.6666259765625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1143103778, + "version": 45, + "versionNonce": 1343517282, + "isDeleted": false, + "boundElementIds": [ + "-lU_z4mfDB58ZiJ8HlTxY", + "LU4D6A2Ugd1V9uKE6SwOC" + ], + "updated": 1639393963543 + }, + { + "id": "rp7H2PQFGWvQJIbz1y8IG", + "type": "rectangle", + "x": 705.3333740234375, + "y": 220.83335876464844, + "width": 24.6666259765625, + "height": 24.6666259765625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 764034658, + "version": 77, + "versionNonce": 1528280446, + "isDeleted": false, + "boundElementIds": [ + "-lU_z4mfDB58ZiJ8HlTxY", + "LU4D6A2Ugd1V9uKE6SwOC", + "n9WWKSJRRhkFG2L3AY6W_", + "4mPQElLVeuU0MBB9zyNTL" + ], + "updated": 1639394052440 + }, + { + "id": "5H4DUHb4ELZWIIpXO32ix", + "type": "rectangle", + "x": 385.3333740234375, + "y": 284.16668701171875, + "width": 22, + "height": 22, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1387104638, + "version": 257, + "versionNonce": 947580094, + "isDeleted": false, + "boundElementIds": [ + "uJx77oj5eyZPw61wszaJN", + "CYPbqJ97T4dK8aTY2NoA6", + "E-k0fg9CKUsCbBcIFgpQN", + "iqQRk3oncpFlTohh4RxWf", + "xQjRQnu2M-Lx4L_FApAWi", + "ZBwjcWgJYIRx-XieGSul2", + "S1o9eYMClf4Mrmfw9HlDs", + "nbIlU5kICCXoOhMWP1aoq" + ], + "updated": 1639393977422 + }, + { + "id": "1-O9JOrs2pnONGNtZiH4B", + "type": "text", + "x": 349.1666564941406, + "y": 482.8332824707031, + "width": 105, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1412742590, + "version": 106, + "versionNonce": 541336190, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "text": "rathole client", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14 + }, + { + "id": "CYPbqJ97T4dK8aTY2NoA6", + "type": "arrow", + "x": 400.5230856224749, + "y": 452.05962166754307, + "width": 3.021429201284718, + "height": 139.89293465582432, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 941766434, + "version": 627, + "versionNonce": 840863074, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "points": [ + [ + 0, + 0 + ], + [ + -3.021429201284718, + -139.89293465582432 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "72LJc8JYfizCW-59n-YiJ", + "focus": 0.012722437706498701, + "gap": 4.777461928511805 + }, + "endBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": -0.07128881792747094, + "gap": 6 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "iqQRk3oncpFlTohh4RxWf", + "type": "arrow", + "x": 411.88175007980965, + "y": 278.4142786269854, + "width": 41.1875896678809, + "height": 38.07827569869187, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 316085374, + "version": 241, + "versionNonce": 705621374, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393963544, + "points": [ + [ + 0, + 0 + ], + [ + 41.1875896678809, + -38.07827569869187 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": -0.111311585930796, + "gap": 7.333343505859375 + }, + "endBinding": { + "elementId": "NzpaVP1cgsvfg6KfdD99G", + "focus": -0.4128416678755755, + "gap": 8.3058554409374 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "xQjRQnu2M-Lx4L_FApAWi", + "type": "arrow", + "x": 415.03538422542744, + "y": 453.3711527236708, + "width": 10.189618736042235, + "height": 139.87115272367078, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 419928446, + "version": 136, + "versionNonce": 325751074, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "points": [ + [ + 0, + 0 + ], + [ + -10.189618736042235, + -139.87115272367078 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "72LJc8JYfizCW-59n-YiJ", + "focus": 0.20468988783315253, + "gap": 9.013184853545113 + }, + "endBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": -0.6081345501761519, + "gap": 7.33331298828125 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ZBwjcWgJYIRx-XieGSul2", + "type": "arrow", + "x": 381.63001651123903, + "y": 456.0383959625252, + "width": 2.158517689316966, + "height": 145.46817028787365, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 39236898, + "version": 133, + "versionNonce": 1006640354, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "points": [ + [ + 0, + 0 + ], + [ + 2.158517689316966, + -145.46817028787365 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "72LJc8JYfizCW-59n-YiJ", + "focus": -0.22319845234228697, + "gap": 8.153168060821436 + }, + "endBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": 1.1032903390278315, + "gap": 4.666656494140625 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "S1o9eYMClf4Mrmfw9HlDs", + "type": "arrow", + "x": 418.66668701171875, + "y": 287.5, + "width": 38.666656494140625, + "height": 43.33331298828125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 247933282, + "version": 30, + "versionNonce": 1800972706, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393973403, + "points": [ + [ + 0, + 0 + ], + [ + 38.666656494140625, + -43.33331298828125 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": 0.7442699983400397, + "gap": 11.33331298828125 + }, + "endBinding": { + "elementId": "NzpaVP1cgsvfg6KfdD99G", + "focus": -0.4020068751542848, + "gap": 10.019774658829391 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "nbIlU5kICCXoOhMWP1aoq", + "type": "arrow", + "x": 403.3333435058594, + "y": 273.5, + "width": 44, + "height": 37.33331298828125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 261563746, + "version": 26, + "versionNonce": 1384498722, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639393977422, + "points": [ + [ + 0, + 0 + ], + [ + 44, + -37.33331298828125 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "5H4DUHb4ELZWIIpXO32ix", + "focus": -0.7734750559093652, + "gap": 10.66668701171875 + }, + "endBinding": { + "elementId": "NzpaVP1cgsvfg6KfdD99G", + "focus": -0.6418956116222861, + "gap": 6.895194123821575 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "7N39v3qK0fltyhClnuI_Q", + "type": "ellipse", + "x": 582.0000305175781, + "y": 444.16668701171875, + "width": 112.6666259765625, + "height": 47.33331298828125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1272534334, + "version": 116, + "versionNonce": 1964223486, + "isDeleted": false, + "boundElementIds": [ + "DjwSuFQtjGNkkF4rl7myd" + ], + "updated": 1639394068076 + }, + { + "id": "THrrqy4Axfy1vlF2wrI9s", + "type": "ellipse", + "x": 582.0000305175781, + "y": 498.8333740234375, + "width": 112.6666259765625, + "height": 47.33331298828125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 614595234, + "version": 142, + "versionNonce": 909853822, + "isDeleted": false, + "boundElementIds": [ + "8qillKpd5VKO0hrasQMVX" + ], + "updated": 1639394068076 + }, + { + "id": "WyAj01yc3DnhvWQG7tHd9", + "type": "text", + "x": 605.8333435058594, + "y": 457.8333435058594, + "width": 65, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2040747710, + "version": 45, + "versionNonce": 172071906, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "text": "service 1", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14 + }, + { + "id": "CDfW7H0EVISeS0Zugsf8W", + "type": "text", + "x": 600.9999694824219, + "y": 512.5000305175781, + "width": 72, + "height": 20, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1292334754, + "version": 74, + "versionNonce": 1763707710, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394067926, + "text": "service 2", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14 + }, + { + "id": "DjwSuFQtjGNkkF4rl7myd", + "type": "arrow", + "x": 493.3333435058594, + "y": 489.0001220703125, + "width": 80.49465291276579, + "height": 19.591364584118082, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1489129058, + "version": 177, + "versionNonce": 1120784162, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394068076, + "points": [ + [ + 0, + 0 + ], + [ + 80.49465291276579, + -19.591364584118082 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "72LJc8JYfizCW-59n-YiJ", + "focus": 0.46790554502387516, + "gap": 7.388222614736868 + }, + "endBinding": { + "elementId": "7N39v3qK0fltyhClnuI_Q", + "focus": 0.5164042977199623, + "gap": 8.24035262874532 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "8qillKpd5VKO0hrasQMVX", + "type": "arrow", + "x": 497.3333435058594, + "y": 496.33331298828125, + "width": 78, + "height": 22, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1551722530, + "version": 144, + "versionNonce": 1536814754, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394068076, + "points": [ + [ + 0, + 0 + ], + [ + 78, + 22 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "72LJc8JYfizCW-59n-YiJ", + "focus": -0.6747942752141096, + "gap": 6.222408426625634 + }, + "endBinding": { + "elementId": "THrrqy4Axfy1vlF2wrI9s", + "focus": -0.4771879886646304, + "gap": 7.177668745668626 + }, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "n9WWKSJRRhkFG2L3AY6W_", + "type": "arrow", + "x": 876.3054169557988, + "y": 206.15009644516743, + "width": 139.63872994408007, + "height": 28.349903554832565, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1631704318, + "version": 50, + "versionNonce": 1307848610, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394048593, + "points": [ + [ + 0, + 0 + ], + [ + -139.63872994408007, + 28.349903554832565 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "rp7H2PQFGWvQJIbz1y8IG", + "focus": 0.3498468485388594, + "gap": 6.66668701171875 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "4mPQElLVeuU0MBB9zyNTL", + "type": "arrow", + "x": 696.6666870117188, + "y": 231.83334350585938, + "width": 140.66668701171875, + "height": 30.666656494140625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1042183102, + "version": 16, + "versionNonce": 392654114, + "isDeleted": false, + "boundElementIds": null, + "updated": 1639394053746, + "points": [ + [ + 0, + 0 + ], + [ + -140.66668701171875, + -30.666656494140625 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "rp7H2PQFGWvQJIbz1y8IG", + "focus": -0.21600645731035134, + "gap": 8.66668701171875 + }, + "endBinding": { + "elementId": "NzpaVP1cgsvfg6KfdD99G", + "focus": -0.31535312984667163, + "gap": 5.787735184532465 + }, + "startArrowhead": null, + "endArrowhead": null + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/doc/img/overview.png b/doc/img/overview.png new file mode 100644 index 0000000..779361d Binary files /dev/null and b/doc/img/overview.png differ diff --git a/doc/img/rathole-logo.png b/doc/img/rathole-logo.png new file mode 100644 index 0000000..3bf0f40 Binary files /dev/null and b/doc/img/rathole-logo.png differ diff --git a/doc/img/tcp_bitrate.svg b/doc/img/tcp_bitrate.svg new file mode 100644 index 0000000..6a3065a --- /dev/null +++ b/doc/img/tcp_bitrate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/img/tcp_latency.svg b/doc/img/tcp_latency.svg new file mode 100644 index 0000000..172e802 --- /dev/null +++ b/doc/img/tcp_latency.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/internals.md b/doc/internals.md new file mode 100644 index 0000000..b0ede5a --- /dev/null +++ b/doc/internals.md @@ -0,0 +1,34 @@ +# Internals + +![overview](./img/overview.png) + +## Conceptions +### Service +The entity whose traffic needs to be forwarded + +### Server +The host that runs `rathole` in the server mode + +### Client +The host behind the NAT that runs `rathole` in the client mode. It has some services that need to be forwarded. + +### Visitor +Who visists a *service*, via the *server* + +### Control Channel +A control channel is a TCP connection between the *server* and the *client* that only carries `rathole` control commands for one *service*. + +### Data Channel + +A data channel is a TCP connection between the *server* and the *client* that only carries the encapsulated data that needs forwarding for one *service*. + +## The Process + +*TODO: Add more details about the protocol* + +When `rathole` starts in the client mode, it creates connections to `server.common.bind_addr` for each service. These connection acts as control channels. + +When a control channel starts, the server challenge the client by a nonce, the client is required to authenticate as the service it wants to represent. Then the forwarding of that service is set up. + +When the server accepts a connection on a service's `bind_port`, it sends a control command to the client via the corresponding contorl channel. Then the client connects to the server to create a data channel. In this way, a forwarding is set up. The server also creates a few data channels in advance to improve the latency. + diff --git a/doc/out-of-scope.md b/doc/out-of-scope.md new file mode 100644 index 0000000..4e699c7 --- /dev/null +++ b/doc/out-of-scope.md @@ -0,0 +1,9 @@ +# Out of Scope + +- *domain based forwarding for HTTP* + + Use nginx to do this. + +- *frp's STCP* + + You may want to consider secure tunnels like wireguard or zerotier. diff --git a/example/minimal/client.toml b/example/minimal/client.toml new file mode 100644 index 0000000..bb5f6b8 --- /dev/null +++ b/example/minimal/client.toml @@ -0,0 +1,6 @@ +[client] +remote_addr = "localhost:2333" +default_token = "123" + +[client.services.foo1] +local_addr = "127.0.0.1:80" diff --git a/example/minimal/server.toml b/example/minimal/server.toml new file mode 100644 index 0000000..1eec033 --- /dev/null +++ b/example/minimal/server.toml @@ -0,0 +1,7 @@ +[server] +bind_addr = "0.0.0.0:2333" +default_token = "123" + +[server.services.foo1] +bind_addr = "0.0.0.0:5202" + diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..13a218e --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,20 @@ +use clap::{AppSettings, Parser}; + +#[derive(Parser, Debug)] +#[clap(about, version, setting(AppSettings::DeriveDisplayOrder))] +pub struct Cli { + /// The path to the configuration file + /// + /// Running as a client or a server is automatically determined + /// according to the configuration file. + #[clap(parse(from_os_str), name = "config")] + pub config_path: std::path::PathBuf, + + /// Run as a server + #[clap(long, short)] + pub server: bool, + + /// Run as a client + #[clap(long, short)] + pub client: bool, +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..fbcc56c --- /dev/null +++ b/src/client.rs @@ -0,0 +1,242 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use crate::config::{ClientConfig, ClientServiceConfig, Config}; +use crate::protocol::{ + self, read_hello, DataChannelCmd, + Hello::{self, *}, + CURRENT_PROTO_VRESION, HASH_WIDTH_IN_BYTES, +}; +use crate::protocol::{read_data_cmd, Ack, Auth, ControlChannelCmd}; +use anyhow::{anyhow, bail, Context, Result}; +use backoff::ExponentialBackoff; +use tokio::io; +use tokio::sync::oneshot; +use tokio::time::{self, Duration}; +use tokio::{self, io::AsyncWriteExt, net::TcpStream}; +use tracing::{debug, error, info, instrument, Instrument, Span}; + +pub async fn run_client(config: &Config) -> Result<()> { + let mut client = Client::from(config)?; + client.run().await +} + +type ServiceDigest = protocol::Digest; +type Nonce = protocol::Digest; + +struct Client<'a> { + config: &'a ClientConfig, + service_handles: HashMap, +} + +impl<'a> Client<'a> { + fn from(config: &'a Config) -> Result { + if let Some(config) = &config.client { + Ok(Client { + config, + service_handles: HashMap::new(), + }) + } else { + Err(anyhow!("Try to run as a client, but the configuration is missing. Please add the `[client]` block")) + } + } + + async fn run(&mut self) -> Result<()> { + for (name, config) in &self.config.services { + let handle = + ControlChannelHandle::new((*config).clone(), self.config.remote_addr.clone()); + self.service_handles.insert(name.clone(), handle); + } + + loop { + tokio::select! { + val = tokio::signal::ctrl_c() => { + match val { + Ok(()) => {} + Err(err) => { + error!("Unable to listen for shutdown signal: {}", err); + } + } + break; + }, + } + } + + // Shutdown all services + for (_, handle) in self.service_handles.drain() { + handle.shutdown(); + } + + Ok(()) + } +} + +struct RunDataChannelArgs { + session_key: Nonce, + remote_addr: String, + local_addr: String, +} + +async fn run_data_channel(args: Arc) -> Result<()> { + // Retry at least every 100ms, at most for 10 seconds + let backoff = ExponentialBackoff { + max_interval: Duration::from_millis(100), + max_elapsed_time: Some(Duration::from_secs(10)), + ..Default::default() + }; + + // Connect to remote_addr + let mut conn = backoff::future::retry(backoff, || async { + Ok(TcpStream::connect(&args.remote_addr) + .await + .with_context(|| "Failed to connect to remote_addr")?) + }) + .await?; + + // Send nonce + let v: &[u8; HASH_WIDTH_IN_BYTES] = args.session_key[..].try_into().unwrap(); + let hello = Hello::DataChannelHello(CURRENT_PROTO_VRESION, v.to_owned()); + conn.write_all(&bincode::serialize(&hello).unwrap()).await?; + + // Forward + match read_data_cmd(&mut conn).await? { + DataChannelCmd::StartForward => { + let mut local = TcpStream::connect(&args.local_addr) + .await + .with_context(|| "Failed to conenct to local_addr")?; + let _ = io::copy_bidirectional(&mut conn, &mut local).await; + } + } + Ok(()) +} + +struct ControlChannel { + digest: ServiceDigest, + service: ClientServiceConfig, + shutdown_rx: oneshot::Receiver, + remote_addr: String, +} + +struct ControlChannelHandle { + shutdown_tx: oneshot::Sender, +} + +impl ControlChannel { + #[instrument(skip(self), fields(service=%self.service.name))] + async fn run(&mut self) -> Result<()> { + let mut conn = TcpStream::connect(&self.remote_addr) + .await + .with_context(|| format!("Failed to connect to the server: {}", &self.remote_addr))?; + + // Send hello + let hello_send = + Hello::ControlChannelHello(CURRENT_PROTO_VRESION, self.digest[..].try_into().unwrap()); + conn.write_all(&bincode::serialize(&hello_send).unwrap()) + .await?; + + // Read hello + let nonce = match read_hello(&mut conn) + .await + .with_context(|| "Failed to read hello from the server")? + { + ControlChannelHello(_, d) => d, + _ => { + bail!("Unexpected type of hello"); + } + }; + + // Send auth + let mut concat = Vec::from(self.service.token.as_ref().unwrap().as_bytes()); + concat.extend_from_slice(&nonce); + + let session_key = protocol::digest(&concat); + let auth = Auth(session_key); + conn.write_all(&bincode::serialize(&auth).unwrap()).await?; + + // Read ack + match protocol::read_ack(&mut conn).await? { + Ack::Ok => {} + v => { + return Err(anyhow!("{}", v)) + .with_context(|| format!("Authentication failed: {}", self.service.name)); + } + } + + // Channel ready + info!("Control channel established"); + + let remote_addr = self.remote_addr.clone(); + let local_addr = self.service.local_addr.clone(); + let data_ch_args = Arc::new(RunDataChannelArgs { + session_key, + remote_addr, + local_addr, + }); + + loop { + tokio::select! { + val = protocol::read_control_cmd(&mut conn) => { + let val = val?; + debug!( "Received {:?}", val); + match val { + ControlChannelCmd::CreateDataChannel => { + let args = data_ch_args.clone(); + tokio::spawn(async move { + if let Err(e) = run_data_channel(args).await.with_context(|| "Failed to run the data channel") { + error!("{:?}", e); + } + }.instrument(Span::current())); + } + } + }, + _ = &mut self.shutdown_rx => { + info!( "Shutting down gracefully..."); + break; + } + } + } + + Ok(()) + } +} + +impl ControlChannelHandle { + #[instrument(skip_all, fields(service = %service.name))] + fn new(service: ClientServiceConfig, remote_addr: String) -> ControlChannelHandle { + let digest = protocol::digest(service.name.as_bytes()); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let mut s = ControlChannel { + digest, + service, + shutdown_rx, + remote_addr, + }; + + tokio::spawn( + async move { + loop { + if let Err(err) = s + .run() + .await + .with_context(|| "Failed to run the control channel") + { + let duration = Duration::from_secs(2); + error!("{:?}\n\nRetry in {:?}...", err, duration); + time::sleep(duration).await; + } else { + // Shutdown + break; + } + } + } + .instrument(Span::current()), + ); + + ControlChannelHandle { shutdown_tx } + } + + fn shutdown(self) { + // A send failure shows that the actor has already shutdown. + let _ = self.shutdown_tx.send(0u8); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..20e3f65 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,124 @@ +use anyhow::{anyhow, bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use tokio::fs; +use toml; + +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum Encryption { + #[serde(rename = "none")] + None, + #[serde(rename = "aes")] + Aes, +} + +fn default_encryption() -> Encryption { + Encryption::None +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ClientServiceConfig { + #[serde(skip)] + pub name: String, + pub local_addr: String, + pub token: Option, + #[serde(default = "default_encryption")] + pub encryption: Encryption, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ServerServiceConfig { + #[serde(skip)] + pub name: String, + pub bind_addr: String, + pub token: Option, + #[serde(default = "default_encryption")] + pub encryption: Encryption, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ClientConfig { + pub remote_addr: String, + pub default_token: Option, + pub services: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ServerConfig { + pub bind_addr: String, + pub default_token: Option, + pub services: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub server: Option, + pub client: Option, +} + +impl Config { + fn from_str(s: &str) -> Result { + let mut config: Config = + toml::from_str(&s).with_context(|| "Failed to parse the config")?; + if let Some(server) = config.server.as_mut() { + for (name, s) in &mut server.services { + s.name = name.clone(); + if s.token.is_none() { + s.token = server.default_token.clone(); + if s.token.is_none() { + bail!("The token of service {} is not set", name); + } + } + } + } + if let Some(client) = config.client.as_mut() { + for (name, s) in &mut client.services { + s.name = name.clone(); + if s.token.is_none() { + s.token = client.default_token.clone(); + if s.token.is_none() { + bail!("The token of service {} is not set", name); + } + } + } + } + if config.server.is_none() && config.client.is_none() { + Err(anyhow!("Neither of `[server]` or `[client]` is defined")) + } else { + Ok(config) + } + } + + pub async fn from_file(path: &PathBuf) -> Result { + let s: String = fs::read_to_string(path) + .await + .with_context(|| format!("Failed to read the config {:?}", path))?; + Config::from_str(&s).with_context(|| { + "Configuration is invalid. Please refer to the configuration specification." + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + use anyhow::Result; + + #[test] + fn test_mimic_client_config() -> Result<()> { + let s = fs::read_to_string("./example/mimic/client.toml").unwrap(); + Config::from_str(&s)?; + Ok(()) + } + + #[test] + fn test_mimic_server_config() -> Result<()> { + let s = fs::read_to_string("./example/mimic/server.toml").unwrap(); + Config::from_str(&s)?; + Ok(()) + } +} diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..ddca7ad --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,15 @@ +use std::time::Duration; + +use anyhow::{Context, Result}; +use socket2::{SockRef, TcpKeepalive}; +use tokio::net::TcpStream; + +// Tokio hesitates to expose this option...So we have to do it on our own :( +// The good news is that using socket2 it can be easily done, without losing portablity. +// See https://github.com/tokio-rs/tokio/issues/3082 +pub fn set_tcp_keepalive(conn: &TcpStream) -> Result<()> { + let s = SockRef::from(conn); + let keepalive = TcpKeepalive::new().with_time(Duration::from_secs(60)); + s.set_tcp_keepalive(&keepalive) + .with_context(|| "Failed to set keepalive") +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..01889b9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,153 @@ +mod cli; +mod client; +mod config; +mod helper; +mod multi_map; +mod protocol; +mod server; + +pub use cli::Cli; +pub use config::Config; + +use anyhow::{anyhow, Result}; +use tracing::debug; + +use client::run_client; +use server::run_server; + +pub async fn run(args: &Cli) -> Result<()> { + let config = Config::from_file(&args.config_path).await?; + + tracing_subscriber::fmt::init(); + + debug!("{:?}", config); + + // Raise `nofile` limit on linux and mac + fdlimit::raise_fd_limit(); + + match determine_run_mode(&config, &args) { + RunMode::Undetermine => Err(anyhow!("Cannot determine running as a server or a client")), + RunMode::Client => run_client(&config).await, + RunMode::Server => run_server(&config).await, + } +} + +#[derive(PartialEq, Eq, Debug)] +enum RunMode { + Server, + Client, + Undetermine, +} + +fn determine_run_mode(config: &Config, args: &Cli) -> RunMode { + use RunMode::*; + if args.client && args.server { + Undetermine + } else { + if args.client { + Client + } else if args.server { + Server + } else { + if config.server.is_some() && config.client.is_none() { + Server + } else if config.client.is_some() && config.server.is_none() { + Client + } else { + Undetermine + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_determine_run_mode() { + use config::*; + use RunMode::*; + + struct T { + cfg_s: bool, + cfg_c: bool, + arg_s: bool, + arg_c: bool, + run_mode: RunMode, + } + + let tests = [ + T { + cfg_s: false, + cfg_c: false, + arg_s: false, + arg_c: false, + run_mode: Undetermine, + }, + T { + cfg_s: true, + cfg_c: false, + arg_s: false, + arg_c: false, + run_mode: Server, + }, + T { + cfg_s: false, + cfg_c: true, + arg_s: false, + arg_c: false, + run_mode: Client, + }, + T { + cfg_s: true, + cfg_c: true, + arg_s: false, + arg_c: false, + run_mode: Undetermine, + }, + T { + cfg_s: true, + cfg_c: true, + arg_s: true, + arg_c: false, + run_mode: Server, + }, + T { + cfg_s: true, + cfg_c: true, + arg_s: false, + arg_c: true, + run_mode: Client, + }, + T { + cfg_s: true, + cfg_c: true, + arg_s: true, + arg_c: true, + run_mode: Undetermine, + }, + ]; + + for t in tests { + let config = Config { + server: match t.cfg_s { + true => Some(ServerConfig::default()), + false => None, + }, + client: match t.cfg_c { + true => Some(ClientConfig::default()), + false => None, + }, + }; + + let args = Cli { + config_path: std::path::PathBuf::new(), + server: t.arg_s, + client: t.arg_c, + }; + + assert_eq!(determine_run_mode(&config, &args), t.run_mode); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3a47d30 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +use anyhow::Result; +use clap::Parser; +use rathole::{run, Cli}; +use tokio; + +#[tokio::main] +async fn main() -> Result<()> { + let args = Cli::parse(); + run(&args).await +} diff --git a/src/multi_map.rs b/src/multi_map.rs new file mode 100644 index 0000000..81af822 --- /dev/null +++ b/src/multi_map.rs @@ -0,0 +1,114 @@ +use std::borrow::Borrow; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; + +struct RawItem(*mut (K1, K2, V)); +unsafe impl Send for RawItem {} +unsafe impl Sync for RawItem {} + +pub struct MultiMap { + map1: HashMap, RawItem>, + map2: HashMap, RawItem>, +} + +struct Key(*const T); + +unsafe impl Send for Key {} +unsafe impl Sync for Key {} + +impl Borrow for Key { + fn borrow(&self) -> &T { + unsafe { &*self.0 } + } +} + +impl Hash for Key { + fn hash(&self, state: &mut H) { + (self.borrow() as &T).hash(state) + } +} + +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + (self.borrow() as &T).eq(other.borrow()) + } +} + +impl Eq for Key {} + +impl MultiMap { + pub fn new() -> Self { + MultiMap { + map1: HashMap::new(), + map2: HashMap::new(), + } + } +} + +#[allow(dead_code)] +impl MultiMap +where + K1: Hash + Eq + Send, + K2: Hash + Eq + Send, + V: Send, +{ + pub fn insert(&mut self, k1: K1, k2: K2, v: V) -> Result<(), (K1, K2, V)> { + if self.map1.contains_key(&k1) || self.map2.contains_key(&k2) { + return Err((k1, k2, v)); + } + let item = Box::new((k1, k2, v)); + let k1 = Key(&item.0); + let k2 = Key(&item.1); + let item = Box::into_raw(item); + self.map1.insert(k1, RawItem(item)); + self.map2.insert(k2, RawItem(item)); + Ok(()) + } + + pub fn get1(&self, k1: &K1) -> Option<&V> { + let item = self.map1.get(k1)?; + let item = unsafe { &*item.0 }; + Some(&item.2) + } + + pub fn get1_mut(&mut self, k1: &K1) -> Option<&mut V> { + let item = self.map1.get(k1)?; + let item = unsafe { &mut *item.0 }; + Some(&mut item.2) + } + + pub fn get2(&self, k2: &K2) -> Option<&V> { + let item = self.map2.get(k2)?; + let item = unsafe { &*item.0 }; + Some(&item.2) + } + + pub fn get_mut2(&mut self, k2: &K2) -> Option<&mut V> { + let item = self.map2.get(k2)?; + let item = unsafe { &mut *item.0 }; + Some(&mut item.2) + } + + pub fn remove1(&mut self, k1: &K1) -> Option { + let item = self.map1.remove(k1)?; + let item = unsafe { Box::from_raw(item.0) }; + self.map2.remove(&item.1); + Some(item.2) + } + + pub fn remove2(&mut self, k2: &K2) -> Option { + let item = self.map2.remove(k2)?; + let item = unsafe { Box::from_raw(item.0) }; + self.map1.remove(&item.0); + Some(item.2) + } +} + +impl Drop for MultiMap { + fn drop(&mut self) { + self.map1.clear(); + self.map2 + .drain() + .for_each(|(_, item)| drop(unsafe { Box::from_raw(item.0) })); + } +} diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..a8801fa --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,137 @@ +pub const HASH_WIDTH_IN_BYTES: usize = 32; +use anyhow::{Context, Result}; +use bincode; + +use lazy_static::lazy_static; + +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncReadExt; +use tokio::net::TcpStream; + +type ProtocolVersion = u8; +const PROTO_V0: u8 = 0u8; + +pub const CURRENT_PROTO_VRESION: ProtocolVersion = PROTO_V0; + +pub type Digest = [u8; HASH_WIDTH_IN_BYTES]; + +#[derive(Deserialize, Serialize, Debug)] +pub enum Hello { + ControlChannelHello(ProtocolVersion, Digest), // sha256sum(service name) or a nonce + DataChannelHello(ProtocolVersion, Digest), // token provided by CreateDataChannel +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct Auth(pub Digest); + +#[derive(Deserialize, Serialize, Debug)] +pub enum Ack { + Ok, + ServiceNotExist, + AuthFailed, +} + +impl std::fmt::Display for Ack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Ack::Ok => "Ok", + Ack::ServiceNotExist => "Service not exist", + Ack::AuthFailed => "Incorrect token", + } + ) + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub enum ControlChannelCmd { + CreateDataChannel, +} + +#[derive(Deserialize, Serialize, Debug)] +pub enum DataChannelCmd { + StartForward, +} + +pub fn digest(data: &[u8]) -> Digest { + let d = ring::digest::digest(&ring::digest::SHA256, data); + d.as_ref().try_into().unwrap() +} + +struct PacketLength { + hello: usize, + ack: usize, + auth: usize, + c_cmd: usize, + d_cmd: usize, +} + +impl PacketLength { + pub fn new() -> PacketLength { + let username = "default"; + let d = digest(username.as_bytes()); + let hello = bincode::serialized_size(&Hello::ControlChannelHello(CURRENT_PROTO_VRESION, d)) + .unwrap() as usize; + let c_cmd = + bincode::serialized_size(&ControlChannelCmd::CreateDataChannel).unwrap() as usize; + let d_cmd = bincode::serialized_size(&DataChannelCmd::StartForward).unwrap() as usize; + let ack = Ack::Ok; + let ack = bincode::serialized_size(&ack).unwrap() as usize; + + let auth = bincode::serialized_size(&Auth(d)).unwrap() as usize; + PacketLength { + hello, + ack, + auth, + c_cmd, + d_cmd, + } + } +} + +lazy_static! { + static ref PACKET_LEN: PacketLength = PacketLength::new(); +} + +pub async fn read_hello(conn: &mut TcpStream) -> Result { + let mut buf = vec![0u8; PACKET_LEN.hello]; + conn.read_exact(&mut buf) + .await + .with_context(|| "Failed to read hello")?; + let hello = bincode::deserialize(&buf).with_context(|| "Failed to deserialize hello")?; + Ok(hello) +} + +pub async fn read_auth(conn: &mut TcpStream) -> Result { + let mut buf = vec![0u8; PACKET_LEN.auth]; + conn.read_exact(&mut buf) + .await + .with_context(|| "Failed to read auth")?; + bincode::deserialize(&buf).with_context(|| "Failed to deserialize auth") +} + +pub async fn read_ack(conn: &mut TcpStream) -> Result { + let mut bytes = vec![0u8; PACKET_LEN.ack]; + conn.read_exact(&mut bytes) + .await + .with_context(|| "Failed to read ack")?; + bincode::deserialize(&bytes).with_context(|| "Failed to deserialize ack") +} + +pub async fn read_control_cmd(conn: &mut TcpStream) -> Result { + let mut bytes = vec![0u8; PACKET_LEN.c_cmd]; + conn.read_exact(&mut bytes) + .await + .with_context(|| "Failed to read control cmd")?; + bincode::deserialize(&bytes).with_context(|| "Failed to deserialize control cmd") +} + +pub async fn read_data_cmd(conn: &mut TcpStream) -> Result { + let mut bytes = vec![0u8; PACKET_LEN.d_cmd]; + conn.read_exact(&mut bytes) + .await + .with_context(|| "Failed to read data cmd")?; + bincode::deserialize(&bytes).with_context(|| "Failed to deserialize data cmd") +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..68abed6 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,389 @@ +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use crate::config::{Config, ServerConfig, ServerServiceConfig}; +use crate::helper::set_tcp_keepalive; +use crate::multi_map::MultiMap; +use crate::protocol::{ + self, read_hello, Hello, Hello::ControlChannelHello, Hello::DataChannelHello, +}; +use crate::protocol::{read_auth, Ack, ControlChannelCmd, DataChannelCmd, HASH_WIDTH_IN_BYTES}; +use anyhow::{anyhow, bail, Context, Result}; +use rand::RngCore; +use tokio::io::{self, AsyncWriteExt}; +use tokio::sync::mpsc; +use tokio::sync::{oneshot, RwLock}; +use tokio::time; +use tokio::{ + self, + net::{self, TcpListener, TcpStream}, +}; +use tracing::{debug, error, info, info_span, warn, Instrument}; + +use backoff::{backoff::Backoff, ExponentialBackoff}; + +type ServiceDigest = protocol::Digest; +type Nonce = protocol::Digest; + +const POOL_SIZE: usize = 64; +const CHAN_SIZE: usize = 2048; + +pub async fn run_server(config: &Config) -> Result<()> { + let mut server = Server::from(config)?; + + server.run().await +} + +type ControlChannelMap = MultiMap; +struct Server<'a> { + config: &'a ServerConfig, + services: Arc>>, + control_channels: Arc>, +} + +impl<'a> Server<'a> { + pub fn from(config: &'a Config) -> Result { + match &config.server { + Some(config) => Ok(Server { + config, + services: Arc::new(RwLock::new(Server::generate_service_hashmap(config))), + control_channels: Arc::new(RwLock::new(ControlChannelMap::new())), + }), + None => + Err(anyhow!("Try to run as a server, but the configuration is missing. Please add the `[server]` block")) + } + } + + fn generate_service_hashmap( + server_config: &ServerConfig, + ) -> HashMap { + let mut ret = HashMap::new(); + for u in &server_config.services { + ret.insert(protocol::digest(u.0.as_bytes()), (*u.1).clone()); + } + ret + } + + pub async fn run(&mut self) -> Result<()> { + let l = net::TcpListener::bind(&self.config.bind_addr) + .await + .with_context(|| "Failed to listen at `server.bind_addr`")?; + info!("Listening at {}", self.config.bind_addr); + + // Retry at least every 100ms + let mut backoff = ExponentialBackoff { + max_interval: Duration::from_millis(100), + max_elapsed_time: None, + ..Default::default() + }; + + // Listen for incoming control or data channels + loop { + tokio::select! { + ret = l.accept() => { + match ret { + Err(err) => { + // Possibly a EMFILE. So sleep for a while and retry + if let Some(d) = backoff.next_backoff() { + error!("Failed to accept: {}. Retry in {:?}...", err, d); + time::sleep(d).await; + } else { + // This branch will never be executed according to the current retry policy + error!("Too many retries. Aborting..."); + break; + } + } + Ok((conn, addr)) => { + backoff.reset(); + debug!("Incomming connection from {}", addr); + let services = self.services.clone(); + let control_channels = self.control_channels.clone(); + tokio::spawn(async move { + if let Err(err) = handle_connection(conn, addr, services, control_channels).await.with_context(||"Failed to handle a connection to `server.bind_addr`") { + error!("{:?}", err); + } + }.instrument(info_span!("handle_connection", %addr))); + } + } + }, + _ = tokio::signal::ctrl_c() => { + info!("Shuting down gracefully..."); + break; + } + } + } + + Ok(()) + } +} + +async fn handle_connection( + mut conn: TcpStream, + addr: SocketAddr, + services: Arc>>, + control_channels: Arc>, +) -> Result<()> { + // Read hello + let hello = read_hello(&mut conn).await?; + match hello { + ControlChannelHello(_, service_digest) => { + info!("New control channel incomming from {}", addr); + + // Generate a nonce + let mut nonce = vec![0u8; HASH_WIDTH_IN_BYTES]; + rand::thread_rng().fill_bytes(&mut nonce); + + // Send hello + let hello_send = Hello::ControlChannelHello( + protocol::CURRENT_PROTO_VRESION, + nonce.clone().try_into().unwrap(), + ); + conn.write_all(&bincode::serialize(&hello_send).unwrap()) + .await?; + + // Lookup the service + let services_guard = services.read().await; + let service_config = match services_guard.get(&service_digest) { + Some(v) => v, + None => { + conn.write_all(&bincode::serialize(&Ack::ServiceNotExist).unwrap()) + .await?; + bail!("No such a service {}", hex::encode(&service_digest)); + } + }; + let service_name = &service_config.name; + + // Calculate the checksum + let mut concat = Vec::from(service_config.token.as_ref().unwrap().as_bytes()); + concat.append(&mut nonce); + + // Read auth + let d = match read_auth(&mut conn).await? { + protocol::Auth(v) => v, + }; + + // Validate + let session_key = protocol::digest(&concat); + if session_key != d { + conn.write_all(&bincode::serialize(&Ack::AuthFailed).unwrap()) + .await?; + debug!( + "Expect {}, but got {}", + hex::encode(session_key), + hex::encode(d) + ); + bail!("Service {} failed the authentication", service_name); + } else { + let mut h = control_channels.write().await; + + if let Some(_) = h.remove1(&service_digest) { + warn!( + "Dropping previous control channel for digest {}", + hex::encode(service_digest) + ); + } + + let service_config = service_config.clone(); + drop(services_guard); + + // Send ack + conn.write_all(&bincode::serialize(&Ack::Ok).unwrap()) + .await?; + + info!(service = %service_config.name, "Control channel established"); + let handle = ControlChannelHandle::new(conn, service_config); + + // Drop the old handle + let _ = h.insert(service_digest, session_key, handle); + } + } + DataChannelHello(_, nonce) => { + // Validate + let control_channels_guard = control_channels.read().await; + match control_channels_guard.get2(&nonce) { + Some(c_ch) => { + if let Err(e) = set_tcp_keepalive(&conn) { + error!("The connection may be unstable! {:?}", e); + } + + // Send the data channel to the corresponding control channel + c_ch.conn_pool.data_ch_tx.send(conn).await?; + } + None => { + warn!("Data channel has incorrect nonce"); + } + } + } + } + Ok(()) +} + +struct ControlChannel { + conn: TcpStream, + service: ServerServiceConfig, + shutdown_rx: oneshot::Receiver, + visitor_tx: mpsc::Sender, +} + +struct ControlChannelHandle { + shutdown_tx: oneshot::Sender, + conn_pool: ConnectionPoolHandle, +} + +impl ControlChannelHandle { + fn new(conn: TcpStream, service: ServerServiceConfig) -> ControlChannelHandle { + let (shutdown_tx, shutdown_rx) = oneshot::channel::(); + let name = service.name.clone(); + let conn_pool = ConnectionPoolHandle::new(); + let actor = ControlChannel { + conn, + shutdown_rx, + service, + visitor_tx: conn_pool.visitor_tx.clone(), + }; + + tokio::spawn(async move { + if let Err(err) = actor.run().await { + error!(%name, "{}", err); + } + }); + + ControlChannelHandle { + shutdown_tx, + conn_pool, + } + } +} + +impl ControlChannel { + #[tracing::instrument(skip(self), fields(service = %self.service.name))] + async fn run(mut self) -> Result<()> { + if let Err(e) = set_tcp_keepalive(&self.conn) { + error!("The connection may be unstable! {:?}", e); + } + + let l = match TcpListener::bind(&self.service.bind_addr).await { + Ok(v) => v, + Err(e) => { + let duration = Duration::from_secs(1); + error!( + "Failed to listen on service.bind_addr: {}. Retry in {:?}...", + e, duration + ); + time::sleep(duration).await; + TcpListener::bind(&self.service.bind_addr).await? + } + }; + + info!("Listening at {}", &self.service.bind_addr); + + let (data_req_tx, mut data_req_rx) = mpsc::unbounded_channel::(); + tokio::spawn(async move { + let cmd = bincode::serialize(&ControlChannelCmd::CreateDataChannel).unwrap(); + while let Some(_) = data_req_rx.recv().await { + if self.conn.write_all(&cmd).await.is_err() { + break; + } + } + }); + + for _i in 0..POOL_SIZE { + if let Err(e) = data_req_tx.send(0) { + error!("Failed to request data channel {}", e); + }; + } + + let mut backoff = ExponentialBackoff { + max_interval: Duration::from_secs(1), + ..Default::default() + }; + loop { + tokio::select! { + val = l.accept() => { + match val { + Err(e) => { + error!("{}. Sleep for a while", e); + if let Some(d) = backoff.next_backoff() { + time::sleep(d).await; + } else { + error!("Too many retries. Aborting..."); + break; + } + }, + Ok((incoming, addr)) => { + if let Err(e) = data_req_tx.send(0) { + error!("{}", e); + break; + }; + + backoff.reset(); + + debug!("New visitor from {}", addr); + + let _ = self.visitor_tx.send(incoming).await; + } + } + }, + _ = &mut self.shutdown_rx => { + break; + } + } + } + info!("Service shuting down"); + + Ok(()) + } +} + +#[derive(Debug)] +struct ConnectionPool { + visitor_rx: mpsc::Receiver, + data_ch_rx: mpsc::Receiver, +} + +struct ConnectionPoolHandle { + visitor_tx: mpsc::Sender, + data_ch_tx: mpsc::Sender, +} + +impl ConnectionPoolHandle { + fn new() -> ConnectionPoolHandle { + let (data_ch_tx, data_ch_rx) = mpsc::channel(CHAN_SIZE * 2); + let (visitor_tx, visitor_rx) = mpsc::channel(CHAN_SIZE); + let conn_pool = ConnectionPool { + data_ch_rx, + visitor_rx, + }; + + tokio::spawn(async move { conn_pool.run().await }); + + ConnectionPoolHandle { + data_ch_tx, + visitor_tx, + } + } +} + +impl ConnectionPool { + #[tracing::instrument] + async fn run(mut self) { + loop { + if let Some(mut visitor) = self.visitor_rx.recv().await { + if let Some(mut ch) = self.data_ch_rx.recv().await { + tokio::spawn(async move { + let cmd = bincode::serialize(&DataChannelCmd::StartForward).unwrap(); + if ch.write_all(&cmd).await.is_ok() { + let _ = io::copy_bidirectional(&mut ch, &mut visitor).await; + } + }); + } else { + break; + } + } else { + break; + } + } + } +}