BREAKING CHANGE(remoteingress): migrate core to Rust, add RemoteIngressHub/RemoteIngressEdge JS bridge, and bump package to v2.0.0

This commit is contained in:
2026-02-16 11:22:23 +00:00
parent a3970edf23
commit a144f5a798
25 changed files with 11564 additions and 3408 deletions

5
rust/.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[target.x86_64-unknown-linux-gnu]
linker = "x86_64-linux-gnu-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

980
rust/Cargo.lock generated Normal file
View File

@@ -0,0 +1,980 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "aws-lc-rs"
version = "1.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "deranged"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4"
dependencies = [
"powerfmt",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "env_filter"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
dependencies = [
"portable-atomic",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rcgen"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "remoteingress-bin"
version = "2.0.0"
dependencies = [
"clap",
"env_logger",
"log",
"remoteingress-core",
"remoteingress-protocol",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "remoteingress-core"
version = "2.0.0"
dependencies = [
"log",
"rcgen",
"remoteingress-protocol",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"tokio",
"tokio-rustls",
]
[[package]]
name = "remoteingress-protocol"
version = "2.0.0"
dependencies = [
"tokio",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

7
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[workspace]
resolver = "2"
members = [
"crates/remoteingress-protocol",
"crates/remoteingress-core",
"crates/remoteingress-bin",
]

View File

@@ -0,0 +1,18 @@
[package]
name = "remoteingress-bin"
version = "2.0.0"
edition = "2021"
[[bin]]
name = "remoteingress-bin"
path = "src/main.rs"
[dependencies]
remoteingress-core = { path = "../remoteingress-core" }
remoteingress-protocol = { path = "../remoteingress-protocol" }
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
env_logger = "0.11"

View File

@@ -0,0 +1,354 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::Mutex;
use remoteingress_core::hub::{AllowedEdge, HubConfig, HubEvent, TunnelHub};
use remoteingress_core::edge::{EdgeConfig, EdgeEvent, TunnelEdge};
#[derive(Parser)]
#[command(name = "remoteingress-bin", version = "2.0.0")]
struct Cli {
/// Run in IPC management mode (JSON over stdin/stdout)
#[arg(long)]
management: bool,
}
// IPC message types
#[derive(Deserialize)]
struct IpcRequest {
id: String,
method: String,
params: serde_json::Value,
}
#[derive(Serialize)]
struct IpcResponse {
id: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
#[derive(Serialize)]
struct IpcEvent {
event: String,
data: serde_json::Value,
}
fn send_ipc_line(line: &str) {
// Write to stdout synchronously, since we're line-buffered
use std::io::Write;
let stdout = std::io::stdout();
let mut out = stdout.lock();
let _ = out.write_all(line.as_bytes());
let _ = out.write_all(b"\n");
let _ = out.flush();
}
fn send_event(event: &str, data: serde_json::Value) {
let evt = IpcEvent {
event: event.to_string(),
data,
};
if let Ok(json) = serde_json::to_string(&evt) {
send_ipc_line(&json);
}
}
fn send_response(id: &str, result: serde_json::Value) {
let resp = IpcResponse {
id: id.to_string(),
success: true,
result: Some(result),
error: None,
};
if let Ok(json) = serde_json::to_string(&resp) {
send_ipc_line(&json);
}
}
fn send_error(id: &str, error: &str) {
let resp = IpcResponse {
id: id.to_string(),
success: false,
result: None,
error: Some(error.to_string()),
};
if let Ok(json) = serde_json::to_string(&resp) {
send_ipc_line(&json);
}
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
if !cli.management {
eprintln!("remoteingress-bin: use --management for IPC mode");
std::process::exit(1);
}
// Initialize logging to stderr (stdout is for IPC)
env_logger::Builder::from_default_env()
.target(env_logger::Target::Stderr)
.filter_level(log::LevelFilter::Info)
.init();
// Send ready event
send_event("ready", serde_json::json!({ "version": "2.0.0" }));
// State
let hub: Arc<Mutex<Option<Arc<TunnelHub>>>> = Arc::new(Mutex::new(None));
let edge: Arc<Mutex<Option<Arc<TunnelEdge>>>> = Arc::new(Mutex::new(None));
// Read commands from stdin
let stdin = tokio::io::stdin();
let reader = BufReader::new(stdin);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let request: IpcRequest = match serde_json::from_str(&line) {
Ok(r) => r,
Err(e) => {
log::error!("Invalid IPC request: {}", e);
continue;
}
};
let hub = hub.clone();
let edge = edge.clone();
tokio::spawn(async move {
handle_request(request, hub, edge).await;
});
}
}
async fn handle_request(
req: IpcRequest,
hub: Arc<Mutex<Option<Arc<TunnelHub>>>>,
edge: Arc<Mutex<Option<Arc<TunnelEdge>>>>,
) {
match req.method.as_str() {
"ping" => {
send_response(&req.id, serde_json::json!({ "pong": true }));
}
"startHub" => {
let config: HubConfig = match serde_json::from_value(req.params.clone()) {
Ok(c) => c,
Err(e) => {
send_error(&req.id, &format!("invalid hub config: {}", e));
return;
}
};
let tunnel_hub = Arc::new(TunnelHub::new(config));
// Forward hub events to IPC
if let Some(mut event_rx) = tunnel_hub.take_event_rx().await {
tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
match &event {
HubEvent::EdgeConnected { edge_id } => {
send_event(
"edgeConnected",
serde_json::json!({ "edgeId": edge_id }),
);
}
HubEvent::EdgeDisconnected { edge_id } => {
send_event(
"edgeDisconnected",
serde_json::json!({ "edgeId": edge_id }),
);
}
HubEvent::StreamOpened {
edge_id,
stream_id,
} => {
send_event(
"streamOpened",
serde_json::json!({
"edgeId": edge_id,
"streamId": stream_id,
}),
);
}
HubEvent::StreamClosed {
edge_id,
stream_id,
} => {
send_event(
"streamClosed",
serde_json::json!({
"edgeId": edge_id,
"streamId": stream_id,
}),
);
}
}
}
});
}
match tunnel_hub.start().await {
Ok(()) => {
*hub.lock().await = Some(tunnel_hub);
send_response(&req.id, serde_json::json!({ "started": true }));
}
Err(e) => {
send_error(&req.id, &format!("failed to start hub: {}", e));
}
}
}
"stopHub" => {
let mut h = hub.lock().await;
if let Some(hub_instance) = h.take() {
hub_instance.stop().await;
send_response(&req.id, serde_json::json!({ "stopped": true }));
} else {
send_response(
&req.id,
serde_json::json!({ "stopped": true, "wasRunning": false }),
);
}
}
"updateAllowedEdges" => {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateEdgesParams {
edges: Vec<AllowedEdge>,
}
let params: UpdateEdgesParams = match serde_json::from_value(req.params.clone()) {
Ok(p) => p,
Err(e) => {
send_error(&req.id, &format!("invalid params: {}", e));
return;
}
};
let h = hub.lock().await;
if let Some(hub_instance) = h.as_ref() {
hub_instance.update_allowed_edges(params.edges).await;
send_response(&req.id, serde_json::json!({ "updated": true }));
} else {
send_error(&req.id, "hub not running");
}
}
"getHubStatus" => {
let h = hub.lock().await;
if let Some(hub_instance) = h.as_ref() {
let status = hub_instance.get_status().await;
send_response(
&req.id,
serde_json::to_value(&status).unwrap_or_default(),
);
} else {
send_response(
&req.id,
serde_json::json!({
"running": false,
"tunnelPort": 0,
"connectedEdges": []
}),
);
}
}
"startEdge" => {
let config: EdgeConfig = match serde_json::from_value(req.params.clone()) {
Ok(c) => c,
Err(e) => {
send_error(&req.id, &format!("invalid edge config: {}", e));
return;
}
};
let tunnel_edge = Arc::new(TunnelEdge::new(config));
// Forward edge events to IPC
if let Some(mut event_rx) = tunnel_edge.take_event_rx().await {
tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
match &event {
EdgeEvent::TunnelConnected => {
send_event("tunnelConnected", serde_json::json!({}));
}
EdgeEvent::TunnelDisconnected => {
send_event("tunnelDisconnected", serde_json::json!({}));
}
EdgeEvent::PublicIpDiscovered { ip } => {
send_event(
"publicIpDiscovered",
serde_json::json!({ "ip": ip }),
);
}
}
}
});
}
match tunnel_edge.start().await {
Ok(()) => {
*edge.lock().await = Some(tunnel_edge);
send_response(&req.id, serde_json::json!({ "started": true }));
}
Err(e) => {
send_error(&req.id, &format!("failed to start edge: {}", e));
}
}
}
"stopEdge" => {
let mut e = edge.lock().await;
if let Some(edge_instance) = e.take() {
edge_instance.stop().await;
send_response(&req.id, serde_json::json!({ "stopped": true }));
} else {
send_response(
&req.id,
serde_json::json!({ "stopped": true, "wasRunning": false }),
);
}
}
"getEdgeStatus" => {
let e = edge.lock().await;
if let Some(edge_instance) = e.as_ref() {
let status = edge_instance.get_status().await;
send_response(
&req.id,
serde_json::to_value(&status).unwrap_or_default(),
);
} else {
send_response(
&req.id,
serde_json::json!({
"running": false,
"connected": false,
"publicIp": null,
"activeStreams": 0,
"listenPorts": []
}),
);
}
}
_ => {
send_error(&req.id, &format!("unknown method: {}", req.method));
}
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "remoteingress-core"
version = "2.0.0"
edition = "2021"
[dependencies]
remoteingress-protocol = { path = "../remoteingress-protocol" }
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.26"
rustls = { version = "0.23", features = ["ring"] }
rcgen = "0.13"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
rustls-pemfile = "2"

View File

@@ -0,0 +1,478 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio_rustls::TlsConnector;
use serde::{Deserialize, Serialize};
use remoteingress_protocol::*;
/// Edge configuration.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EdgeConfig {
pub hub_host: String,
pub hub_port: u16,
pub edge_id: String,
pub secret: String,
pub listen_ports: Vec<u16>,
pub stun_interval_secs: Option<u64>,
}
/// Events emitted by the edge.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum EdgeEvent {
TunnelConnected,
TunnelDisconnected,
#[serde(rename_all = "camelCase")]
PublicIpDiscovered { ip: String },
}
/// Edge status response.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EdgeStatus {
pub running: bool,
pub connected: bool,
pub public_ip: Option<String>,
pub active_streams: usize,
pub listen_ports: Vec<u16>,
}
/// The tunnel edge that listens for client connections and multiplexes them to the hub.
pub struct TunnelEdge {
config: RwLock<EdgeConfig>,
event_tx: mpsc::UnboundedSender<EdgeEvent>,
event_rx: Mutex<Option<mpsc::UnboundedReceiver<EdgeEvent>>>,
shutdown_tx: Mutex<Option<mpsc::Sender<()>>>,
running: RwLock<bool>,
connected: Arc<RwLock<bool>>,
public_ip: Arc<RwLock<Option<String>>>,
active_streams: Arc<AtomicU32>,
next_stream_id: Arc<AtomicU32>,
}
impl TunnelEdge {
pub fn new(config: EdgeConfig) -> Self {
let (event_tx, event_rx) = mpsc::unbounded_channel();
Self {
config: RwLock::new(config),
event_tx,
event_rx: Mutex::new(Some(event_rx)),
shutdown_tx: Mutex::new(None),
running: RwLock::new(false),
connected: Arc::new(RwLock::new(false)),
public_ip: Arc::new(RwLock::new(None)),
active_streams: Arc::new(AtomicU32::new(0)),
next_stream_id: Arc::new(AtomicU32::new(1)),
}
}
/// Take the event receiver (can only be called once).
pub async fn take_event_rx(&self) -> Option<mpsc::UnboundedReceiver<EdgeEvent>> {
self.event_rx.lock().await.take()
}
/// Get the current edge status.
pub async fn get_status(&self) -> EdgeStatus {
EdgeStatus {
running: *self.running.read().await,
connected: *self.connected.read().await,
public_ip: self.public_ip.read().await.clone(),
active_streams: self.active_streams.load(Ordering::Relaxed) as usize,
listen_ports: self.config.read().await.listen_ports.clone(),
}
}
/// Start the edge: connect to hub, start listeners.
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = self.config.read().await.clone();
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(1);
*self.shutdown_tx.lock().await = Some(shutdown_tx);
*self.running.write().await = true;
let connected = self.connected.clone();
let public_ip = self.public_ip.clone();
let active_streams = self.active_streams.clone();
let next_stream_id = self.next_stream_id.clone();
let event_tx = self.event_tx.clone();
tokio::spawn(async move {
edge_main_loop(
config,
connected,
public_ip,
active_streams,
next_stream_id,
event_tx,
shutdown_rx,
)
.await;
});
Ok(())
}
/// Stop the edge.
pub async fn stop(&self) {
if let Some(tx) = self.shutdown_tx.lock().await.take() {
let _ = tx.send(()).await;
}
*self.running.write().await = false;
*self.connected.write().await = false;
}
}
async fn edge_main_loop(
config: EdgeConfig,
connected: Arc<RwLock<bool>>,
public_ip: Arc<RwLock<Option<String>>>,
active_streams: Arc<AtomicU32>,
next_stream_id: Arc<AtomicU32>,
event_tx: mpsc::UnboundedSender<EdgeEvent>,
mut shutdown_rx: mpsc::Receiver<()>,
) {
let mut backoff_ms: u64 = 1000;
let max_backoff_ms: u64 = 30000;
loop {
// Try to connect to hub
let result = connect_to_hub_and_run(
&config,
&connected,
&public_ip,
&active_streams,
&next_stream_id,
&event_tx,
&mut shutdown_rx,
)
.await;
*connected.write().await = false;
let _ = event_tx.send(EdgeEvent::TunnelDisconnected);
active_streams.store(0, Ordering::Relaxed);
match result {
EdgeLoopResult::Shutdown => break,
EdgeLoopResult::Reconnect => {
log::info!("Reconnecting in {}ms...", backoff_ms);
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)) => {}
_ = shutdown_rx.recv() => break,
}
backoff_ms = (backoff_ms * 2).min(max_backoff_ms);
}
}
}
}
enum EdgeLoopResult {
Shutdown,
Reconnect,
}
async fn connect_to_hub_and_run(
config: &EdgeConfig,
connected: &Arc<RwLock<bool>>,
public_ip: &Arc<RwLock<Option<String>>>,
active_streams: &Arc<AtomicU32>,
next_stream_id: &Arc<AtomicU32>,
event_tx: &mpsc::UnboundedSender<EdgeEvent>,
shutdown_rx: &mut mpsc::Receiver<()>,
) -> EdgeLoopResult {
// Build TLS connector that skips cert verification (auth is via secret)
let tls_config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertVerifier))
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(tls_config));
let addr = format!("{}:{}", config.hub_host, config.hub_port);
let tcp = match TcpStream::connect(&addr).await {
Ok(s) => s,
Err(e) => {
log::error!("Failed to connect to hub at {}: {}", addr, e);
return EdgeLoopResult::Reconnect;
}
};
let server_name = rustls::pki_types::ServerName::try_from(config.hub_host.clone())
.unwrap_or_else(|_| rustls::pki_types::ServerName::try_from("remoteingress-hub".to_string()).unwrap());
let tls_stream = match connector.connect(server_name, tcp).await {
Ok(s) => s,
Err(e) => {
log::error!("TLS handshake failed: {}", e);
return EdgeLoopResult::Reconnect;
}
};
let (read_half, mut write_half) = tokio::io::split(tls_stream);
// Send auth line
let auth_line = format!("EDGE {} {}\n", config.edge_id, config.secret);
if write_half.write_all(auth_line.as_bytes()).await.is_err() {
return EdgeLoopResult::Reconnect;
}
*connected.write().await = true;
let _ = event_tx.send(EdgeEvent::TunnelConnected);
log::info!("Connected to hub at {}", addr);
// Start STUN discovery
let stun_interval = config.stun_interval_secs.unwrap_or(300);
let public_ip_clone = public_ip.clone();
let event_tx_clone = event_tx.clone();
let stun_handle = tokio::spawn(async move {
loop {
if let Some(ip) = crate::stun::discover_public_ip().await {
let mut pip = public_ip_clone.write().await;
let changed = pip.as_ref() != Some(&ip);
*pip = Some(ip.clone());
if changed {
let _ = event_tx_clone.send(EdgeEvent::PublicIpDiscovered { ip });
}
}
tokio::time::sleep(std::time::Duration::from_secs(stun_interval)).await;
}
});
// Client socket map: stream_id -> sender for writing data back to client
let client_writers: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>> =
Arc::new(Mutex::new(HashMap::new()));
// Shared tunnel writer
let tunnel_writer = Arc::new(Mutex::new(write_half));
// Start TCP listeners for each port
let mut listener_handles = Vec::new();
for &port in &config.listen_ports {
let tunnel_writer = tunnel_writer.clone();
let client_writers = client_writers.clone();
let active_streams = active_streams.clone();
let next_stream_id = next_stream_id.clone();
let edge_id = config.edge_id.clone();
let handle = tokio::spawn(async move {
let listener = match TcpListener::bind(("0.0.0.0", port)).await {
Ok(l) => l,
Err(e) => {
log::error!("Failed to bind port {}: {}", port, e);
return;
}
};
log::info!("Listening on port {}", port);
loop {
match listener.accept().await {
Ok((client_stream, client_addr)) => {
let stream_id = next_stream_id.fetch_add(1, Ordering::Relaxed);
let tunnel_writer = tunnel_writer.clone();
let client_writers = client_writers.clone();
let active_streams = active_streams.clone();
let edge_id = edge_id.clone();
active_streams.fetch_add(1, Ordering::Relaxed);
tokio::spawn(async move {
handle_client_connection(
client_stream,
client_addr,
stream_id,
port,
&edge_id,
tunnel_writer,
client_writers,
)
.await;
active_streams.fetch_sub(1, Ordering::Relaxed);
});
}
Err(e) => {
log::error!("Accept error on port {}: {}", port, e);
}
}
}
});
listener_handles.push(handle);
}
// Read frames from hub
let mut frame_reader = FrameReader::new(read_half);
let result = loop {
tokio::select! {
frame_result = frame_reader.next_frame() => {
match frame_result {
Ok(Some(frame)) => {
match frame.frame_type {
FRAME_DATA_BACK => {
let writers = client_writers.lock().await;
if let Some(tx) = writers.get(&frame.stream_id) {
let _ = tx.send(frame.payload).await;
}
}
FRAME_CLOSE_BACK => {
let mut writers = client_writers.lock().await;
writers.remove(&frame.stream_id);
}
_ => {
log::warn!("Unexpected frame type {} from hub", frame.frame_type);
}
}
}
Ok(None) => {
log::info!("Hub disconnected (EOF)");
break EdgeLoopResult::Reconnect;
}
Err(e) => {
log::error!("Hub frame error: {}", e);
break EdgeLoopResult::Reconnect;
}
}
}
_ = shutdown_rx.recv() => {
break EdgeLoopResult::Shutdown;
}
}
};
// Cleanup
stun_handle.abort();
for h in listener_handles {
h.abort();
}
result
}
async fn handle_client_connection(
client_stream: TcpStream,
client_addr: std::net::SocketAddr,
stream_id: u32,
dest_port: u16,
edge_id: &str,
tunnel_writer: Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::client::TlsStream<TcpStream>>>>,
client_writers: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>>,
) {
let client_ip = client_addr.ip().to_string();
let client_port = client_addr.port();
// Determine edge IP (use 0.0.0.0 as placeholder — hub doesn't use it for routing)
let edge_ip = "0.0.0.0";
// Send OPEN frame with PROXY v1 header
let proxy_header = build_proxy_v1_header(&client_ip, edge_ip, client_port, dest_port);
let open_frame = encode_frame(stream_id, FRAME_OPEN, proxy_header.as_bytes());
{
let mut w = tunnel_writer.lock().await;
if w.write_all(&open_frame).await.is_err() {
return;
}
}
// Set up channel for data coming back from hub
let (back_tx, mut back_rx) = mpsc::channel::<Vec<u8>>(256);
{
let mut writers = client_writers.lock().await;
writers.insert(stream_id, back_tx);
}
let (mut client_read, mut client_write) = client_stream.into_split();
// Task: hub -> client
let hub_to_client = tokio::spawn(async move {
while let Some(data) = back_rx.recv().await {
if client_write.write_all(&data).await.is_err() {
break;
}
}
let _ = client_write.shutdown().await;
});
// Task: client -> hub
let mut buf = vec![0u8; 32768];
loop {
match client_read.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
let data_frame = encode_frame(stream_id, FRAME_DATA, &buf[..n]);
let mut w = tunnel_writer.lock().await;
if w.write_all(&data_frame).await.is_err() {
break;
}
}
Err(_) => break,
}
}
// Send CLOSE frame
let close_frame = encode_frame(stream_id, FRAME_CLOSE, &[]);
{
let mut w = tunnel_writer.lock().await;
let _ = w.write_all(&close_frame).await;
}
// Cleanup
{
let mut writers = client_writers.lock().await;
writers.remove(&stream_id);
}
hub_to_client.abort();
let _ = edge_id; // used for logging context
}
/// TLS certificate verifier that accepts any certificate (auth is via shared secret).
#[derive(Debug)]
struct NoCertVerifier;
impl rustls::client::danger::ServerCertVerifier for NoCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ED448,
]
}
}

View File

@@ -0,0 +1,477 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio_rustls::TlsAcceptor;
use serde::{Deserialize, Serialize};
use remoteingress_protocol::*;
/// Hub configuration.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HubConfig {
pub tunnel_port: u16,
pub target_host: Option<String>,
#[serde(skip)]
pub tls_cert_pem: Option<String>,
#[serde(skip)]
pub tls_key_pem: Option<String>,
}
impl Default for HubConfig {
fn default() -> Self {
Self {
tunnel_port: 8443,
target_host: Some("127.0.0.1".to_string()),
tls_cert_pem: None,
tls_key_pem: None,
}
}
}
/// An allowed edge identity.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AllowedEdge {
pub id: String,
pub secret: String,
}
/// Runtime status of a connected edge.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConnectedEdgeStatus {
pub edge_id: String,
pub connected_at: u64,
pub active_streams: usize,
}
/// Events emitted by the hub.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum HubEvent {
#[serde(rename_all = "camelCase")]
EdgeConnected { edge_id: String },
#[serde(rename_all = "camelCase")]
EdgeDisconnected { edge_id: String },
#[serde(rename_all = "camelCase")]
StreamOpened { edge_id: String, stream_id: u32 },
#[serde(rename_all = "camelCase")]
StreamClosed { edge_id: String, stream_id: u32 },
}
/// Hub status response.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HubStatus {
pub running: bool,
pub tunnel_port: u16,
pub connected_edges: Vec<ConnectedEdgeStatus>,
}
/// The tunnel hub that accepts edge connections and demuxes streams to SmartProxy.
pub struct TunnelHub {
config: RwLock<HubConfig>,
allowed_edges: Arc<RwLock<HashMap<String, String>>>, // id -> secret
connected_edges: Arc<Mutex<HashMap<String, ConnectedEdgeInfo>>>,
event_tx: mpsc::UnboundedSender<HubEvent>,
event_rx: Mutex<Option<mpsc::UnboundedReceiver<HubEvent>>>,
shutdown_tx: Mutex<Option<mpsc::Sender<()>>>,
running: RwLock<bool>,
}
struct ConnectedEdgeInfo {
connected_at: u64,
active_streams: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>>,
}
impl TunnelHub {
pub fn new(config: HubConfig) -> Self {
let (event_tx, event_rx) = mpsc::unbounded_channel();
Self {
config: RwLock::new(config),
allowed_edges: Arc::new(RwLock::new(HashMap::new())),
connected_edges: Arc::new(Mutex::new(HashMap::new())),
event_tx,
event_rx: Mutex::new(Some(event_rx)),
shutdown_tx: Mutex::new(None),
running: RwLock::new(false),
}
}
/// Take the event receiver (can only be called once).
pub async fn take_event_rx(&self) -> Option<mpsc::UnboundedReceiver<HubEvent>> {
self.event_rx.lock().await.take()
}
/// Update the list of allowed edges.
pub async fn update_allowed_edges(&self, edges: Vec<AllowedEdge>) {
let mut map = self.allowed_edges.write().await;
map.clear();
for edge in edges {
map.insert(edge.id, edge.secret);
}
}
/// Get the current hub status.
pub async fn get_status(&self) -> HubStatus {
let running = *self.running.read().await;
let config = self.config.read().await;
let edges = self.connected_edges.lock().await;
let mut connected = Vec::new();
for (id, info) in edges.iter() {
let streams = info.active_streams.lock().await;
connected.push(ConnectedEdgeStatus {
edge_id: id.clone(),
connected_at: info.connected_at,
active_streams: streams.len(),
});
}
HubStatus {
running,
tunnel_port: config.tunnel_port,
connected_edges: connected,
}
}
/// Start the hub — listen for TLS connections from edges.
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = self.config.read().await.clone();
let tls_config = build_tls_config(&config)?;
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
let listener = TcpListener::bind(("0.0.0.0", config.tunnel_port)).await?;
log::info!("Hub listening on port {}", config.tunnel_port);
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
*self.shutdown_tx.lock().await = Some(shutdown_tx);
*self.running.write().await = true;
let allowed = self.allowed_edges.clone();
let connected = self.connected_edges.clone();
let event_tx = self.event_tx.clone();
let target_host = config.target_host.unwrap_or_else(|| "127.0.0.1".to_string());
tokio::spawn(async move {
loop {
tokio::select! {
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
log::info!("Edge connection from {}", addr);
let acceptor = acceptor.clone();
let allowed = allowed.clone();
let connected = connected.clone();
let event_tx = event_tx.clone();
let target = target_host.clone();
tokio::spawn(async move {
if let Err(e) = handle_edge_connection(
stream, acceptor, allowed, connected, event_tx, target,
).await {
log::error!("Edge connection error: {}", e);
}
});
}
Err(e) => {
log::error!("Accept error: {}", e);
}
}
}
_ = shutdown_rx.recv() => {
log::info!("Hub shutting down");
break;
}
}
}
});
Ok(())
}
/// Stop the hub.
pub async fn stop(&self) {
if let Some(tx) = self.shutdown_tx.lock().await.take() {
let _ = tx.send(()).await;
}
*self.running.write().await = false;
// Clear connected edges
self.connected_edges.lock().await.clear();
}
}
/// Handle a single edge connection: authenticate, then enter frame loop.
async fn handle_edge_connection(
stream: TcpStream,
acceptor: TlsAcceptor,
allowed: Arc<RwLock<HashMap<String, String>>>,
connected: Arc<Mutex<HashMap<String, ConnectedEdgeInfo>>>,
event_tx: mpsc::UnboundedSender<HubEvent>,
target_host: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let tls_stream = acceptor.accept(stream).await?;
let (read_half, write_half) = tokio::io::split(tls_stream);
let mut buf_reader = BufReader::new(read_half);
// Read auth line: "EDGE <edgeId> <secret>\n"
let mut auth_line = String::new();
buf_reader.read_line(&mut auth_line).await?;
let auth_line = auth_line.trim();
let parts: Vec<&str> = auth_line.splitn(3, ' ').collect();
if parts.len() != 3 || parts[0] != "EDGE" {
return Err("invalid auth line".into());
}
let edge_id = parts[1].to_string();
let secret = parts[2];
// Verify credentials
{
let edges = allowed.read().await;
match edges.get(&edge_id) {
Some(expected) => {
if !constant_time_eq(secret.as_bytes(), expected.as_bytes()) {
return Err(format!("invalid secret for edge {}", edge_id).into());
}
}
None => {
return Err(format!("unknown edge {}", edge_id).into());
}
}
}
log::info!("Edge {} authenticated", edge_id);
let _ = event_tx.send(HubEvent::EdgeConnected {
edge_id: edge_id.clone(),
});
// Track this edge
let streams: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>> =
Arc::new(Mutex::new(HashMap::new()));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
{
let mut edges = connected.lock().await;
edges.insert(
edge_id.clone(),
ConnectedEdgeInfo {
connected_at: now,
active_streams: streams.clone(),
},
);
}
// Shared writer for sending frames back to edge
let write_half = Arc::new(Mutex::new(write_half));
// Frame reading loop
let mut frame_reader = FrameReader::new(buf_reader);
loop {
match frame_reader.next_frame().await {
Ok(Some(frame)) => {
match frame.frame_type {
FRAME_OPEN => {
// Payload is PROXY v1 header line
let proxy_header = String::from_utf8_lossy(&frame.payload).to_string();
// Parse destination port from PROXY header
let dest_port = parse_dest_port_from_proxy(&proxy_header).unwrap_or(443);
let stream_id = frame.stream_id;
let edge_id_clone = edge_id.clone();
let event_tx_clone = event_tx.clone();
let streams_clone = streams.clone();
let writer_clone = write_half.clone();
let target = target_host.clone();
let _ = event_tx.send(HubEvent::StreamOpened {
edge_id: edge_id.clone(),
stream_id,
});
// Create channel for data from edge to this stream
let (data_tx, mut data_rx) = mpsc::channel::<Vec<u8>>(256);
{
let mut s = streams.lock().await;
s.insert(stream_id, data_tx);
}
// Spawn task: connect to SmartProxy, send PROXY header, pipe data
tokio::spawn(async move {
let result = async {
let mut upstream =
TcpStream::connect((target.as_str(), dest_port)).await?;
upstream.write_all(proxy_header.as_bytes()).await?;
let (mut up_read, mut up_write) =
upstream.into_split();
// Forward data from edge (via channel) to SmartProxy
let writer_for_edge_data = tokio::spawn(async move {
while let Some(data) = data_rx.recv().await {
if up_write.write_all(&data).await.is_err() {
break;
}
}
let _ = up_write.shutdown().await;
});
// Forward data from SmartProxy back to edge
let mut buf = vec![0u8; 32768];
loop {
match up_read.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
let frame =
encode_frame(stream_id, FRAME_DATA_BACK, &buf[..n]);
let mut w = writer_clone.lock().await;
if w.write_all(&frame).await.is_err() {
break;
}
}
Err(_) => break,
}
}
// Send CLOSE_BACK to edge
let close_frame = encode_frame(stream_id, FRAME_CLOSE_BACK, &[]);
let mut w = writer_clone.lock().await;
let _ = w.write_all(&close_frame).await;
writer_for_edge_data.abort();
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}
.await;
if let Err(e) = result {
log::error!("Stream {} error: {}", stream_id, e);
// Send CLOSE_BACK on error
let close_frame = encode_frame(stream_id, FRAME_CLOSE_BACK, &[]);
let mut w = writer_clone.lock().await;
let _ = w.write_all(&close_frame).await;
}
// Clean up stream
{
let mut s = streams_clone.lock().await;
s.remove(&stream_id);
}
let _ = event_tx_clone.send(HubEvent::StreamClosed {
edge_id: edge_id_clone,
stream_id,
});
});
}
FRAME_DATA => {
let s = streams.lock().await;
if let Some(tx) = s.get(&frame.stream_id) {
let _ = tx.send(frame.payload).await;
}
}
FRAME_CLOSE => {
let mut s = streams.lock().await;
s.remove(&frame.stream_id);
}
_ => {
log::warn!("Unexpected frame type {} from edge", frame.frame_type);
}
}
}
Ok(None) => {
log::info!("Edge {} disconnected (EOF)", edge_id);
break;
}
Err(e) => {
log::error!("Edge {} frame error: {}", edge_id, e);
break;
}
}
}
// Cleanup
{
let mut edges = connected.lock().await;
edges.remove(&edge_id);
}
let _ = event_tx.send(HubEvent::EdgeDisconnected {
edge_id: edge_id.clone(),
});
Ok(())
}
/// Parse destination port from PROXY v1 header.
fn parse_dest_port_from_proxy(header: &str) -> Option<u16> {
let parts: Vec<&str> = header.trim().split_whitespace().collect();
if parts.len() >= 6 {
parts[5].parse().ok()
} else {
None
}
}
/// Build TLS server config from PEM strings, or auto-generate self-signed.
fn build_tls_config(
config: &HubConfig,
) -> Result<rustls::ServerConfig, Box<dyn std::error::Error + Send + Sync>> {
let (cert_pem, key_pem) = match (&config.tls_cert_pem, &config.tls_key_pem) {
(Some(cert), Some(key)) => (cert.clone(), key.clone()),
_ => {
// Generate self-signed certificate
let cert = rcgen::generate_simple_self_signed(vec!["remoteingress-hub".to_string()])?;
let cert_pem = cert.cert.pem();
let key_pem = cert.key_pair.serialize_pem();
(cert_pem, key_pem)
}
};
let certs = rustls_pemfile_parse_certs(&cert_pem)?;
let key = rustls_pemfile_parse_key(&key_pem)?;
let mut config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
config.alpn_protocols = vec![b"remoteingress".to_vec()];
Ok(config)
}
fn rustls_pemfile_parse_certs(
pem: &str,
) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, Box<dyn std::error::Error + Send + Sync>>
{
let mut reader = std::io::Cursor::new(pem.as_bytes());
let certs = rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()?;
Ok(certs)
}
fn rustls_pemfile_parse_key(
pem: &str,
) -> Result<rustls::pki_types::PrivateKeyDer<'static>, Box<dyn std::error::Error + Send + Sync>> {
let mut reader = std::io::Cursor::new(pem.as_bytes());
let key = rustls_pemfile::private_key(&mut reader)?
.ok_or("no private key found in PEM")?;
Ok(key)
}
/// Constant-time comparison of two byte slices.
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}

View File

@@ -0,0 +1,5 @@
pub mod hub;
pub mod edge;
pub mod stun;
pub use remoteingress_protocol as protocol;

View File

@@ -0,0 +1,137 @@
use std::net::Ipv4Addr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
const STUN_SERVER: &str = "stun.cloudflare.com:3478";
const STUN_TIMEOUT: Duration = Duration::from_secs(3);
// STUN constants
const STUN_BINDING_REQUEST: u16 = 0x0001;
const STUN_MAGIC_COOKIE: u32 = 0x2112A442;
const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020;
const ATTR_MAPPED_ADDRESS: u16 = 0x0001;
/// Discover our public IP via STUN Binding Request (RFC 5389).
/// Returns `None` on timeout or parse failure.
pub async fn discover_public_ip() -> Option<String> {
discover_public_ip_from(STUN_SERVER).await
}
pub async fn discover_public_ip_from(server: &str) -> Option<String> {
let result = timeout(STUN_TIMEOUT, async {
let socket = UdpSocket::bind("0.0.0.0:0").await.ok()?;
socket.connect(server).await.ok()?;
// Build STUN Binding Request (20 bytes)
let mut request = [0u8; 20];
// Message Type: Binding Request (0x0001)
request[0..2].copy_from_slice(&STUN_BINDING_REQUEST.to_be_bytes());
// Message Length: 0 (no attributes)
request[2..4].copy_from_slice(&0u16.to_be_bytes());
// Magic Cookie
request[4..8].copy_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
// Transaction ID: 12 random bytes
let txn_id: [u8; 12] = rand_bytes();
request[8..20].copy_from_slice(&txn_id);
socket.send(&request).await.ok()?;
let mut buf = [0u8; 512];
let n = socket.recv(&mut buf).await.ok()?;
if n < 20 {
return None;
}
parse_stun_response(&buf[..n], &txn_id)
})
.await;
match result {
Ok(ip) => ip,
Err(_) => None, // timeout
}
}
fn parse_stun_response(data: &[u8], _txn_id: &[u8; 12]) -> Option<String> {
if data.len() < 20 {
return None;
}
// Verify it's a Binding Response (0x0101)
let msg_type = u16::from_be_bytes([data[0], data[1]]);
if msg_type != 0x0101 {
return None;
}
let msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
let magic = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
// Parse attributes
let attrs = &data[20..std::cmp::min(20 + msg_len, data.len())];
let mut offset = 0;
while offset + 4 <= attrs.len() {
let attr_type = u16::from_be_bytes([attrs[offset], attrs[offset + 1]]);
let attr_len = u16::from_be_bytes([attrs[offset + 2], attrs[offset + 3]]) as usize;
offset += 4;
if offset + attr_len > attrs.len() {
break;
}
let attr_data = &attrs[offset..offset + attr_len];
match attr_type {
ATTR_XOR_MAPPED_ADDRESS if attr_data.len() >= 8 => {
let family = attr_data[1];
if family == 0x01 {
// IPv4
let port_xored = u16::from_be_bytes([attr_data[2], attr_data[3]]);
let _port = port_xored ^ (STUN_MAGIC_COOKIE >> 16) as u16;
let ip_xored = u32::from_be_bytes([
attr_data[4],
attr_data[5],
attr_data[6],
attr_data[7],
]);
let ip = ip_xored ^ magic;
return Some(Ipv4Addr::from(ip).to_string());
}
}
ATTR_MAPPED_ADDRESS if attr_data.len() >= 8 => {
let family = attr_data[1];
if family == 0x01 {
// IPv4 (non-XOR fallback)
let ip = u32::from_be_bytes([
attr_data[4],
attr_data[5],
attr_data[6],
attr_data[7],
]);
return Some(Ipv4Addr::from(ip).to_string());
}
}
_ => {}
}
// Pad to 4-byte boundary
offset += (attr_len + 3) & !3;
}
None
}
/// Generate 12 random bytes for transaction ID.
fn rand_bytes() -> [u8; 12] {
let mut bytes = [0u8; 12];
// Use a simple approach: mix timestamp + counter
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let nanos = now.as_nanos();
bytes[0..8].copy_from_slice(&(nanos as u64).to_le_bytes());
// Fill remaining with process-id based data
let pid = std::process::id();
bytes[8..12].copy_from_slice(&pid.to_le_bytes());
bytes
}

View File

@@ -0,0 +1,7 @@
[package]
name = "remoteingress-protocol"
version = "2.0.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["io-util"] }

View File

@@ -0,0 +1,172 @@
use tokio::io::{AsyncRead, AsyncReadExt};
// Frame type constants
pub const FRAME_OPEN: u8 = 0x01;
pub const FRAME_DATA: u8 = 0x02;
pub const FRAME_CLOSE: u8 = 0x03;
pub const FRAME_DATA_BACK: u8 = 0x04;
pub const FRAME_CLOSE_BACK: u8 = 0x05;
// Frame header size: 4 (stream_id) + 1 (type) + 4 (length) = 9 bytes
pub const FRAME_HEADER_SIZE: usize = 9;
// Maximum payload size (16 MB)
pub const MAX_PAYLOAD_SIZE: u32 = 16 * 1024 * 1024;
/// A single multiplexed frame.
#[derive(Debug, Clone)]
pub struct Frame {
pub stream_id: u32,
pub frame_type: u8,
pub payload: Vec<u8>,
}
/// Encode a frame into bytes: [stream_id:4][type:1][length:4][payload]
pub fn encode_frame(stream_id: u32, frame_type: u8, payload: &[u8]) -> Vec<u8> {
let len = payload.len() as u32;
let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + payload.len());
buf.extend_from_slice(&stream_id.to_be_bytes());
buf.push(frame_type);
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(payload);
buf
}
/// Build a PROXY protocol v1 header line.
/// Format: `PROXY TCP4 <client_ip> <edge_ip> <client_port> <dest_port>\r\n`
pub fn build_proxy_v1_header(
client_ip: &str,
edge_ip: &str,
client_port: u16,
dest_port: u16,
) -> String {
format!(
"PROXY TCP4 {} {} {} {}\r\n",
client_ip, edge_ip, client_port, dest_port
)
}
/// Stateful async frame reader that yields `Frame` values from an `AsyncRead`.
pub struct FrameReader<R> {
reader: R,
header_buf: [u8; FRAME_HEADER_SIZE],
}
impl<R: AsyncRead + Unpin> FrameReader<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
header_buf: [0u8; FRAME_HEADER_SIZE],
}
}
/// Read the next frame. Returns `None` on EOF, `Err` on protocol violation.
pub async fn next_frame(&mut self) -> Result<Option<Frame>, std::io::Error> {
// Read header
match self.reader.read_exact(&mut self.header_buf).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
let stream_id = u32::from_be_bytes([
self.header_buf[0],
self.header_buf[1],
self.header_buf[2],
self.header_buf[3],
]);
let frame_type = self.header_buf[4];
let length = u32::from_be_bytes([
self.header_buf[5],
self.header_buf[6],
self.header_buf[7],
self.header_buf[8],
]);
if length > MAX_PAYLOAD_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("frame payload too large: {} bytes", length),
));
}
let mut payload = vec![0u8; length as usize];
if length > 0 {
self.reader.read_exact(&mut payload).await?;
}
Ok(Some(Frame {
stream_id,
frame_type,
payload,
}))
}
/// Consume the reader and return the inner stream.
pub fn into_inner(self) -> R {
self.reader
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_frame() {
let data = b"hello";
let encoded = encode_frame(42, FRAME_DATA, data);
assert_eq!(encoded.len(), FRAME_HEADER_SIZE + data.len());
// stream_id = 42 in BE
assert_eq!(&encoded[0..4], &42u32.to_be_bytes());
// frame type
assert_eq!(encoded[4], FRAME_DATA);
// length
assert_eq!(&encoded[5..9], &5u32.to_be_bytes());
// payload
assert_eq!(&encoded[9..], b"hello");
}
#[test]
fn test_encode_empty_frame() {
let encoded = encode_frame(1, FRAME_CLOSE, &[]);
assert_eq!(encoded.len(), FRAME_HEADER_SIZE);
assert_eq!(&encoded[5..9], &0u32.to_be_bytes());
}
#[test]
fn test_proxy_v1_header() {
let header = build_proxy_v1_header("1.2.3.4", "5.6.7.8", 12345, 443);
assert_eq!(header, "PROXY TCP4 1.2.3.4 5.6.7.8 12345 443\r\n");
}
#[tokio::test]
async fn test_frame_reader() {
let frame1 = encode_frame(1, FRAME_OPEN, b"PROXY TCP4 1.2.3.4 5.6.7.8 1234 443\r\n");
let frame2 = encode_frame(1, FRAME_DATA, b"GET / HTTP/1.1\r\n");
let frame3 = encode_frame(1, FRAME_CLOSE, &[]);
let mut data = Vec::new();
data.extend_from_slice(&frame1);
data.extend_from_slice(&frame2);
data.extend_from_slice(&frame3);
let cursor = std::io::Cursor::new(data);
let mut reader = FrameReader::new(cursor);
let f1 = reader.next_frame().await.unwrap().unwrap();
assert_eq!(f1.stream_id, 1);
assert_eq!(f1.frame_type, FRAME_OPEN);
assert!(f1.payload.starts_with(b"PROXY"));
let f2 = reader.next_frame().await.unwrap().unwrap();
assert_eq!(f2.frame_type, FRAME_DATA);
let f3 = reader.next_frame().await.unwrap().unwrap();
assert_eq!(f3.frame_type, FRAME_CLOSE);
assert!(f3.payload.is_empty());
// EOF
assert!(reader.next_frame().await.unwrap().is_none());
}
}