fix(server): generate correct public IP for client configs instead of 0.0.0.0

This commit is contained in:
ospab 2026-05-27 18:17:11 +03:00
parent ac91665263
commit 9ac0908c1e
22 changed files with 903 additions and 1097 deletions

455
Cargo.lock generated
View File

@ -111,6 +111,30 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -211,6 +235,19 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.2"
@ -229,26 +266,6 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "c2rust-bitfields"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41"
dependencies = [
"c2rust-bitfields-derive",
]
[[package]]
name = "c2rust-bitfields-derive"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.62"
@ -356,7 +373,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -381,6 +398,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -396,6 +422,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@ -439,7 +471,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -471,7 +503,7 @@ dependencies = [
"proc-macro-error2", "proc-macro-error2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -511,7 +543,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -527,9 +559,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]]
name = "etherparse"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d8a704b617484e9d867a0423cd45f7577f008c4068e2e33378f8d3860a6d73"
dependencies = [
"arrayvec",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]] [[package]]
name = "fiat-crypto" name = "fiat-crypto"
version = "0.2.9" version = "0.2.9"
@ -557,6 +625,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -564,6 +647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -572,6 +656,33 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.32" version = "0.3.32"
@ -580,7 +691,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -601,9 +712,13 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
] ]
@ -834,7 +949,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.2", "windows-core",
] ]
[[package]] [[package]]
@ -976,12 +1091,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "ioctl-sys"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c"
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.12.0" version = "2.12.0"
@ -1041,7 +1150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -1076,15 +1185,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.185" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.9" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-link", "windows-link",
@ -1096,6 +1205,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.29"
@ -1162,6 +1280,34 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "netstack-smoltcp"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "398691cef792b89eb5d29e6ea6b3999def706b908d355e29815ba8101cf5c4c8"
dependencies = [
"etherparse",
"futures",
"rand 0.8.5",
"smoltcp",
"spin",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@ -1232,9 +1378,12 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"chrono", "chrono",
"futures",
"futures-util", "futures-util",
"hmac", "hmac",
"json_comments", "json_comments",
"libc",
"netstack-smoltcp",
"ostp-core", "ostp-core",
"portable-atomic", "portable-atomic",
"rand 0.8.5", "rand 0.8.5",
@ -1243,14 +1392,12 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"smoltcp",
"socket2", "socket2",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tracing", "tracing",
"tun", "tun",
"webpki-roots 0.26.11", "webpki-roots 0.26.11",
"wintun 0.4.0",
] ]
[[package]] [[package]]
@ -1282,6 +1429,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -1328,6 +1477,12 @@ dependencies = [
"winres", "winres",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "pem" name = "pem"
version = "3.0.6" version = "3.0.6"
@ -1350,6 +1505,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "poly1305" name = "poly1305"
version = "0.8.0" version = "0.8.0"
@ -1410,7 +1576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -1432,7 +1598,7 @@ dependencies = [
"proc-macro-error-attr2", "proc-macro-error-attr2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -1681,7 +1847,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.117", "syn",
"walkdir", "walkdir",
] ]
@ -1767,6 +1933,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.28" version = "1.0.28"
@ -1800,7 +1972,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -1889,15 +2061,16 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "smoltcp" name = "smoltcp"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a1a996951e50b5971a2c8c0fa05a381480d70a933064245c4a223ddc87ccc97" checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"byteorder", "byteorder",
"cfg-if", "cfg-if",
"defmt 0.3.100", "defmt 0.3.100",
"heapless", "heapless",
"log",
"managed", "managed",
] ]
@ -1927,6 +2100,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -1945,17 +2127,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@ -1984,7 +2155,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2013,7 +2184,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2024,7 +2195,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2082,9 +2253,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.0" version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@ -2104,7 +2275,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2205,7 +2376,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2255,20 +2426,23 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "tun" name = "tun"
version = "0.6.1" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0adb9992bbd5ca76f3847ed579ad4ee8defb2ec2eea918cceef17ccc66fa4fd4" checksum = "bb51dd4da3b9e08057abbaed588d4244bccb0a4122788aff2775b9a4b9aec489"
dependencies = [ dependencies = [
"byteorder",
"bytes", "bytes",
"cfg-if",
"futures",
"futures-core", "futures-core",
"ioctl-sys", "ipnet",
"libc", "libc",
"log", "log",
"thiserror 1.0.69", "nix",
"thiserror 2.0.18",
"tokio", "tokio",
"tokio-util", "tokio-util",
"wintun 0.3.2", "windows-sys 0.61.2",
"wintun-bindings",
] ]
[[package]] [[package]]
@ -2444,7 +2618,7 @@ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2535,45 +2709,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
]
[[package]]
name = "windows"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
dependencies = [
"windows-core 0.51.1",
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
] ]
[[package]] [[package]]
@ -2597,7 +2733,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2608,7 +2744,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -2677,21 +2813,6 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "windows_x86_64_msvc 0.42.2",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -2714,12 +2835,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2732,12 +2847,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@ -2750,12 +2859,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -2774,12 +2877,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@ -2792,12 +2889,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@ -2810,12 +2901,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2828,12 +2913,6 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@ -2850,29 +2929,17 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wintun" name = "wintun-bindings"
version = "0.3.2" version = "0.7.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" checksum = "4d94caf41ce60c37c9215c68f124835c401d2ec4f9b3b6431b491c79b3cdd335"
dependencies = [ dependencies = [
"c2rust-bitfields", "blocking",
"futures",
"libloading", "libloading",
"log", "log",
"thiserror 1.0.69", "thiserror 2.0.18",
"windows 0.51.1", "windows-sys 0.61.2",
]
[[package]]
name = "wintun"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b3c8c8876c686f8a2d6376999ac1c9a24c74d2968551c9394f7e89127783685"
dependencies = [
"c2rust-bitfields",
"libloading",
"log",
"thiserror 1.0.69",
"windows 0.52.0",
] ]
[[package]] [[package]]
@ -2911,7 +2978,7 @@ dependencies = [
"heck", "heck",
"indexmap", "indexmap",
"prettyplease", "prettyplease",
"syn 2.0.117", "syn",
"wasm-metadata", "wasm-metadata",
"wit-bindgen-core", "wit-bindgen-core",
"wit-component", "wit-component",
@ -2927,7 +2994,7 @@ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
"wit-bindgen-core", "wit-bindgen-core",
"wit-bindgen-rust", "wit-bindgen-rust",
] ]
@ -3003,7 +3070,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
"synstructure", "synstructure",
] ]
@ -3024,7 +3091,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]
@ -3044,7 +3111,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
"synstructure", "synstructure",
] ]
@ -3084,7 +3151,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn",
] ]
[[package]] [[package]]

View File

@ -25,11 +25,7 @@ sha2 = "0.10.8"
base64 = "0.22.1" base64 = "0.22.1"
webpki-roots = "0.26" webpki-roots = "0.26"
rustls-pki-types = "1.7" rustls-pki-types = "1.7"
smoltcp = { version = "0.11", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "socket-tcp", "socket-udp"] } tun = { version = "0.8.9", features = ["async"] }
netstack-smoltcp = "0.2.2"
[target.'cfg(target_os = "windows")'.dependencies] futures = "0.3.32"
wintun = "0.4" libc = "0.2.186"
[target.'cfg(target_os = "linux")'.dependencies]
tun = { version = "0.6", features = ["async"] }

View File

@ -20,8 +20,12 @@ pub struct ClientConfig {
#[serde(default)] #[serde(default)]
pub multiplex: MultiplexConfig, pub multiplex: MultiplexConfig,
pub dns_server: Option<String>, pub dns_server: Option<String>,
#[serde(default = "default_tun_stack")]
pub tun_stack: String,
} }
fn default_tun_stack() -> String { "system".to_string() }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExclusionConfig { pub struct ExclusionConfig {
#[serde(default)] #[serde(default)]
@ -142,6 +146,7 @@ impl Default for ClientConfig {
exclusions: ExclusionConfig::default(), exclusions: ExclusionConfig::default(),
multiplex: MultiplexConfig::default(), multiplex: MultiplexConfig::default(),
dns_server: None, dns_server: None,
tun_stack: "system".to_string(),
} }
} }
} }
@ -184,6 +189,7 @@ struct RawTransportSection {
struct RawTunSection { struct RawTunSection {
enable: Option<bool>, enable: Option<bool>,
dns: Option<String>, dns: Option<String>,
stack: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -275,6 +281,7 @@ impl ClientConfig {
sessions: mux.sessions.unwrap_or(1), sessions: mux.sessions.unwrap_or(1),
}, },
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()), dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()),
}) })
} }

View File

@ -198,6 +198,7 @@ pub async fn run_client_core(
let (ui_tx, mut ui_rx) = mpsc::channel(512); let (ui_tx, mut ui_rx) = mpsc::channel(512);
let (cmd_tx, cmd_rx) = mpsc::channel(128); let (cmd_tx, cmd_rx) = mpsc::channel(128);
let (shutdown_tx, shutdown_rx) = watch::channel(false); let (shutdown_tx, shutdown_rx) = watch::channel(false);
let proxy_shutdown_rx = shutdown_tx.subscribe();
// Auto-connect on startup // Auto-connect on startup
@ -247,9 +248,7 @@ pub async fn run_client_core(
}); });
let config_clone = config.clone(); let config_clone = config.clone();
let (mut proxy_task, mut wintun_task) = if config.mode == "proxy" { let mut proxy_task = tokio::spawn(async move {
let proxy_shutdown_rx = shutdown_tx.subscribe();
let t = tokio::spawn(async move {
tunnel::run_local_proxy( tunnel::run_local_proxy(
config.local_proxy, config.local_proxy,
config.ostp, config.ostp,
@ -261,15 +260,14 @@ pub async fn run_client_core(
) )
.await .await
}); });
(Some(t), None)
} else if config.mode == "tun" {
let wintun_shutdown_rx = shutdown_tx.subscribe(); let wintun_shutdown_rx = shutdown_tx.subscribe();
let t = tokio::spawn(async move { let mut wintun_task = if config_clone.mode == "tun" {
tunnel::run_tun_tunnel(config_clone, proxy_events_tx, client_msgs_rx, wintun_shutdown_rx).await Some(tokio::spawn(async move {
}); tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx).await
(None, Some(t)) }))
} else { } else {
(None, None) None
}; };
// Wait for either external shutdown OR any task to fail // Wait for either external shutdown OR any task to fail
@ -282,9 +280,7 @@ pub async fn run_client_core(
let _ = shutdown_tx.send(true); let _ = shutdown_tx.send(true);
res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??; res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??;
} }
res = async { res = &mut proxy_task => {
if let Some(ref mut t) = proxy_task { t.await } else { std::future::pending().await }
} => {
let _ = shutdown_tx.send(true); let _ = shutdown_tx.send(true);
res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??; res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??;
} }
@ -298,9 +294,7 @@ pub async fn run_client_core(
// Final cleanup: wait for tasks to finish // Final cleanup: wait for tasks to finish
let _ = bridge_task.await; let _ = bridge_task.await;
if let Some(task) = proxy_task { let _ = proxy_task.await;
let _ = task.await;
}
if let Some(task) = wintun_task { if let Some(task) = wintun_task {
let _ = task.await; let _ = task.await;
} }

View File

@ -1,29 +1,27 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use tokio::sync::{mpsc, watch}; use tokio::sync::watch;
#[cfg(target_os = "linux")]
use tracing::info;
use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
#[cfg(target_os = "linux")]
use crate::tunnel::tun_device::create_tun_device;
#[cfg(target_os = "linux")]
use crate::tunnel::smoltcp_stack::run_smoltcp_stack;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::process::Command; use std::process::{Command, Stdio, Child};
#[cfg(target_os = "linux")]
use std::io::{BufRead, BufReader};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
struct LinuxRouteGuard { struct LinuxRouteGuard {
server_ip_str: String, server_ip_str: String,
default_gw: String, default_gw: String,
default_if: String, default_if: String,
child: Option<Child>,
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
impl Drop for LinuxRouteGuard { impl Drop for LinuxRouteGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
}
let cleanup_script = format!( let cleanup_script = format!(
"ip route del 0.0.0.0/1 dev ostp_tun || true; \ "ip route del 0.0.0.0/1 dev ostp_tun || true; \
ip route del 128.0.0.0/1 dev ostp_tun || true; \ ip route del 128.0.0.0/1 dev ostp_tun || true; \
@ -41,13 +39,39 @@ impl Drop for LinuxRouteGuard {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn run_linux_tunnel( pub async fn run_linux_tunnel(
config: crate::config::ClientConfig, config: crate::config::ClientConfig,
proxy_events_tx: mpsc::Sender<ProxyEvent>,
client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
info!("Initializing built-in Linux TUN tunnel..."); let debug = config.debug;
if debug {
println!("[ostp] Initializing TUN tunnel...");
}
// 1. Pre-flight system checks let exe = std::env::current_exe()?;
let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?;
let mut tun2socks_exe = dir.join("tun2socks");
if !tun2socks_exe.exists() {
// Try system PATH via standard command check
let in_path = Command::new("which")
.arg("tun2socks")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if in_path {
tun2socks_exe = std::path::PathBuf::from("tun2socks");
} else {
return Err(anyhow!(
"CRITICAL: 'tun2socks' binary is missing!\n\
OSTP requires tun2socks for TUN mode on Linux. Please download the appropriate binary for your architecture from: \n\
https://github.com/xjasonlyu/tun2socks/releases \n\
and place it in the same directory as the ostp executable ({}), or install it globally in your PATH.",
dir.display()
));
}
}
// 1.5. Pre-flight system checks
let is_root = Command::new("id") let is_root = Command::new("id")
.arg("-u") .arg("-u")
.output() .output()
@ -76,7 +100,10 @@ pub async fn run_linux_tunnel(
.ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?; .ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?;
let server_ip_str = server_ip.to_string(); let server_ip_str = server_ip.to_string();
info!("Resolved server IP: {}", server_ip_str);
if debug {
println!("[ostp] Resolved server IP: {}", server_ip_str);
}
// 3. Detect current default gateway and interface // 3. Detect current default gateway and interface
let route_output = Command::new("sh") let route_output = Command::new("sh")
@ -87,6 +114,7 @@ pub async fn run_linux_tunnel(
let route_str = String::from_utf8_lossy(&route_output.stdout); let route_str = String::from_utf8_lossy(&route_output.stdout);
let parts: Vec<&str> = route_str.split_whitespace().collect(); let parts: Vec<&str> = route_str.split_whitespace().collect();
// Expected: "default via 192.168.1.1 dev eth0 ..."
let mut default_gw = String::new(); let mut default_gw = String::new();
let mut default_if = String::new(); let mut default_if = String::new();
@ -103,62 +131,95 @@ pub async fn run_linux_tunnel(
return Err(anyhow!("Failed to discover active default gateway or network interface on Linux system.")); return Err(anyhow!("Failed to discover active default gateway or network interface on Linux system."));
} }
info!("Default route: gateway={} interface={}", default_gw, default_if); if debug {
println!("[ostp] Default route: gateway={} interface={}", default_gw, default_if);
}
// Create the TunDevice inside the client process (creates the interface and sets up IP/MTU/Status) // 4. Setup commands (Using standard /1 routing trick for fail-proof overriding)
let tun_dev = create_tun_device("ostp_tun", config.ostp.mtu)?;
let mut _guard = LinuxRouteGuard {
server_ip_str: server_ip_str.clone(),
default_gw: default_gw.clone(),
default_if: default_if.clone(),
};
// 4. Setup routing rules
let setup_script = format!( let setup_script = format!(
"ip route add {} via {} dev {}; \ "ip tuntap add name ostp_tun mode tun || true; \
ip link set dev ostp_tun mtu {}; \
ip addr add 10.1.0.2/24 dev ostp_tun || true; \
ip link set dev ostp_tun up; \
ip route add {} via {} dev {}; \
ip route add 1.1.1.1 via {} dev {}; \ ip route add 1.1.1.1 via {} dev {}; \
ip route add 0.0.0.0/1 dev ostp_tun; \ ip route add 0.0.0.0/1 dev ostp_tun; \
ip route add 128.0.0.0/1 dev ostp_tun", ip route add 128.0.0.0/1 dev ostp_tun",
server_ip_str, default_gw, default_if, config.ostp.mtu, server_ip_str, default_gw, default_if,
default_gw, default_if default_gw, default_if
); );
if debug {
println!("[ostp] Executing Linux network config: {}", setup_script);
}
let out = Command::new("sh") let out = Command::new("sh")
.args(["-c", &setup_script]) .args(["-c", &setup_script])
.output()?; .output()?;
if !out.status.success() { if !out.status.success() && debug {
tracing::warn!("Warning: Setup routing returned: {}", String::from_utf8_lossy(&out.stderr)); println!("[ostp] Warning: Setup routing returned: {}", String::from_utf8_lossy(&out.stderr));
} }
info!("TUN tunnel active. Direct in-process packets handling started."); // 5. Prepare and launch tun2socks
// Using HTTP Proxy natively avoids any UDP Associate requests,
// providing clean TCP proxying with maximum reliability.
let proxy_url = format!("http://{}", config.local_proxy.bind_addr);
// Run the smoltcp stack loop in the background if debug {
let stack_shutdown_rx = shutdown.clone(); println!("[ostp] Spawning {} -device ostp_tun -proxy {}", tun2socks_exe.display(), proxy_url);
let stack_handle = tokio::spawn(async move { }
if let Err(e) = run_smoltcp_stack(
tun_dev.packet_rx, let mut child = Command::new(&tun2socks_exe)
tun_dev.packet_tx, .args([
config.ostp.mtu, "-device", "ostp_tun",
proxy_events_tx, "-proxy", &proxy_url,
client_msgs_rx, ])
stack_shutdown_rx, .stdout(if debug { Stdio::piped() } else { Stdio::null() })
).await { .stderr(if debug { Stdio::piped() } else { Stdio::null() })
tracing::error!("smoltcp stack loop failed: {:?}", e); .spawn()
.map_err(|e| anyhow!("Failed to spawn tun2socks process: {}", e))?;
let mut _guard = LinuxRouteGuard {
server_ip_str: server_ip_str.clone(),
default_gw: default_gw.clone(),
default_if: default_if.clone(),
child: None,
};
println!("[ostp] TUN tunnel active. All traffic is routed through OSTP.");
if debug {
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
tokio::spawn(async move {
let reader = BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
println!("[tun2socks] {}", line);
} }
}); });
// 5. Wait for shutdown signal tokio::spawn(async move {
let reader = BufReader::new(stderr);
for line in reader.lines().map_while(Result::ok) {
tracing::warn!("tun2socks: {}", line);
}
});
}
_guard.child = Some(child);
// 6. Wait for shutdown signal
let _ = shutdown.changed().await; let _ = shutdown.changed().await;
info!("Deactivating TUN tunnel..."); println!("[ostp] Deactivating TUN tunnel...");
// Drop guard runs cleanup automatically
drop(_guard); drop(_guard);
// Terminate smoltcp stack println!("[ostp] TUN tunnel stopped.");
let _ = stack_handle.await;
info!("TUN tunnel stopped.");
Ok(()) Ok(())
} }
@ -166,8 +227,6 @@ pub async fn run_linux_tunnel(
#[allow(dead_code)] #[allow(dead_code)]
pub async fn run_linux_tunnel( pub async fn run_linux_tunnel(
_config: crate::config::ClientConfig, _config: crate::config::ClientConfig,
_proxy_events_tx: mpsc::Sender<ProxyEvent>,
_client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
_shutdown: watch::Receiver<bool>, _shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
Err(anyhow!("Linux tunnel driver executed on a non-Linux host!")) Err(anyhow!("Linux tunnel driver executed on a non-Linux host!"))

View File

@ -1,31 +1,30 @@
mod proxy; mod proxy;
mod wintun_handler; mod wintun_handler;
mod linux_handler; mod linux_handler;
mod tun_device; pub mod native_handler;
mod smoltcp_stack;
pub async fn run_tun_tunnel( pub async fn run_tun_tunnel(
config: crate::config::ClientConfig, config: crate::config::ClientConfig,
proxy_events_tx: mpsc::Sender<ProxyEvent>,
client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
shutdown: watch::Receiver<bool>, shutdown: watch::Receiver<bool>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if config.tun_stack == "ostp" {
return native_handler::run_native_tunnel(config, shutdown).await;
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
wintun_handler::run_wintun_tunnel(config, proxy_events_tx, client_msgs_rx, shutdown).await wintun_handler::run_wintun_tunnel(config, shutdown).await
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
linux_handler::run_linux_tunnel(config, proxy_events_tx, client_msgs_rx, shutdown).await linux_handler::run_linux_tunnel(config, shutdown).await
} }
#[cfg(not(any(target_os = "windows", target_os = "linux")))] #[cfg(not(any(target_os = "windows", target_os = "linux")))]
{ {
let _ = shutdown; let _ = shutdown;
let _ = config; let _ = config;
let _ = proxy_events_tx;
let _ = client_msgs_rx;
anyhow::bail!("Operating system unsupported, text an issue at github."); anyhow::bail!("Operating system unsupported, text an issue at github.");
} }
} }
@ -72,7 +71,4 @@ pub async fn run_local_proxy(
run_local_socks5_proxy(cfg, ostp, exclusions, debug, shutdown, proxy_events_tx, client_msgs_rx).await run_local_socks5_proxy(cfg, ostp, exclusions, debug, shutdown, proxy_events_tx, client_msgs_rx).await
} }
pub use tun_device::create_tun_device_from_fd;
pub use smoltcp_stack::run_smoltcp_stack;

View File

@ -10,7 +10,7 @@ use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
pub async fn run_local_socks5_proxy( pub async fn run_local_socks5_proxy(
cfg: LocalProxyConfig, cfg: LocalProxyConfig,
_ostp: OstpConfig, ostp: OstpConfig,
exclusions: ExclusionConfig, exclusions: ExclusionConfig,
debug: bool, debug: bool,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
@ -29,6 +29,7 @@ pub async fn run_local_socks5_proxy(
let matcher = ExclusionMatcher::new(&exclusions); let matcher = ExclusionMatcher::new(&exclusions);
let (connect_tx, mut connect_rx) = mpsc::channel(128); let (connect_tx, mut connect_rx) = mpsc::channel(128);
let max_chunk = ostp.mtu.saturating_sub(150).max(512);
let mut next_stream_id: u16 = 1; let mut next_stream_id: u16 = 1;
let mut active_streams: HashMap<u16, mpsc::UnboundedSender<ProxyToClientMsg>> = HashMap::new(); let mut active_streams: HashMap<u16, mpsc::UnboundedSender<ProxyToClientMsg>> = HashMap::new();
@ -66,6 +67,7 @@ pub async fn run_local_socks5_proxy(
connect_timeout, connect_timeout,
debug, debug,
matcher_clone, matcher_clone,
max_chunk,
).await { ).await {
let msg = err.to_string(); let msg = err.to_string();
// Suppress routine disconnects and unsupported SOCKS5 command attempts (like UDP) from spam logs // Suppress routine disconnects and unsupported SOCKS5 command attempts (like UDP) from spam logs
@ -147,6 +149,7 @@ async fn handle_proxy_client(
connect_timeout: Duration, connect_timeout: Duration,
debug: bool, debug: bool,
matcher: ExclusionMatcher, matcher: ExclusionMatcher,
max_chunk: usize,
) -> Result<()> { ) -> Result<()> {
let _guard = StreamGuard { stream_id, close_tx: close_tx.clone() }; let _guard = StreamGuard { stream_id, close_tx: close_tx.clone() };
@ -340,7 +343,7 @@ async fn handle_proxy_client(
} }
// ── Bidirectional raw data forwarding ───────────────────────────── // ── Bidirectional raw data forwarding ─────────────────────────────
let mut tcp_buf = vec![0_u8; 1024]; let mut tcp_buf = vec![0_u8; 65536];
loop { loop {
tokio::select! { tokio::select! {
read_res = client.read(&mut tcp_buf) => { read_res = client.read(&mut tcp_buf) => {
@ -353,10 +356,15 @@ async fn handle_proxy_client(
break; break;
} }
Ok(n) => { Ok(n) => {
let mut offset = 0;
while offset < n {
let end = (offset + max_chunk).min(n);
let _ = event_tx.send(ProxyEvent::Data { let _ = event_tx.send(ProxyEvent::Data {
stream_id, stream_id,
payload: bytes::Bytes::copy_from_slice(&tcp_buf[..n]), payload: bytes::Bytes::copy_from_slice(&tcp_buf[offset..end]),
}).await; }).await;
offset = end;
}
} }
Err(_) => { Err(_) => {
let _ = event_tx.send(ProxyEvent::Close { stream_id }).await; let _ = event_tx.send(ProxyEvent::Close { stream_id }).await;

View File

@ -1,272 +0,0 @@
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
use anyhow::Result;
use tokio::sync::mpsc;
use tokio::sync::watch;
use bytes::Bytes;
use tracing::{info, error, debug};
use smoltcp::iface::{Config, Interface, SocketSet, SocketHandle};
use smoltcp::phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken};
use smoltcp::socket::tcp::{Socket as TcpSocket, State as TcpState};
use smoltcp::wire::{IpAddress, IpCidr, Ipv4Packet, IpProtocol, TcpPacket};
use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
// Custom smoltcp device that bridges to tokio channels
struct ChannelDevice {
rx_queue: VecDeque<Vec<u8>>,
tx_sender: mpsc::Sender<Vec<u8>>,
capabilities: DeviceCapabilities,
}
impl ChannelDevice {
fn new(tx_sender: mpsc::Sender<Vec<u8>>, mtu: usize) -> Self {
let mut caps = DeviceCapabilities::default();
caps.medium = Medium::Ip;
caps.max_transmission_unit = mtu;
Self {
rx_queue: VecDeque::new(),
tx_sender,
capabilities: caps,
}
}
}
struct ChannelRxToken(Vec<u8>);
impl RxToken for ChannelRxToken {
fn consume<R, F>(mut self, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
f(&mut self.0)
}
}
struct ChannelTxToken(mpsc::Sender<Vec<u8>>);
impl TxToken for ChannelTxToken {
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
let mut buffer = vec![0; len];
let result = f(&mut buffer);
let _ = self.0.try_send(buffer);
result
}
}
impl Device for ChannelDevice {
type RxToken<'a> = ChannelRxToken where Self: 'a;
type TxToken<'a> = ChannelTxToken where Self: 'a;
fn receive(&mut self, _timestamp: smoltcp::time::Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
self.rx_queue.pop_front().map(|packet| {
(
ChannelRxToken(packet),
ChannelTxToken(self.tx_sender.clone()),
)
})
}
fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
Some(ChannelTxToken(self.tx_sender.clone()))
}
fn capabilities(&self) -> DeviceCapabilities {
self.capabilities.clone()
}
}
struct VirtualStream {
_stream_id: u16,
socket_handle: SocketHandle,
last_activity: Instant,
target: String,
established: bool,
}
pub async fn run_smoltcp_stack(
mut tun_rx: mpsc::Receiver<Vec<u8>>,
tun_tx: mpsc::Sender<Vec<u8>>,
mtu: usize,
proxy_events_tx: mpsc::Sender<ProxyEvent>,
mut client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
mut shutdown: watch::Receiver<bool>,
) -> Result<()> {
let mut device = ChannelDevice::new(tun_tx, mtu);
let config = Config::new(smoltcp::wire::HardwareAddress::Ip);
let mut interface = Interface::new(config, &mut device, smoltcp::time::Instant::now());
interface.set_any_ip(true); // Required to intercept all packets
interface.update_ip_addrs(|addrs| {
let _ = addrs.push(IpCidr::new(IpAddress::v4(10, 1, 0, 2), 24));
});
let mut sockets = SocketSet::new(vec![]);
let mut stream_id_counter: u16 = 1;
let mut streams: HashMap<u16, VirtualStream> = HashMap::new();
let mut handle_to_stream_id: HashMap<SocketHandle, u16> = HashMap::new();
// Map to route incoming data from client_msgs_rx to target sockets
let mut pending_client_msgs: VecDeque<(u16, ProxyToClientMsg)> = VecDeque::new();
let mut ticker = tokio::time::interval(std::time::Duration::from_millis(5));
info!("smoltcp virtual TCP/IP stack runner active.");
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
_ = ticker.tick() => {
// Periodical stack poll
}
pkt_opt = tun_rx.recv() => {
if let Some(pkt) = pkt_opt {
// Check if it's a new TCP connection (SYN) and dynamically spawn a listener
if let Ok(ipv4_packet) = Ipv4Packet::new_checked(&pkt) {
if ipv4_packet.next_header() == IpProtocol::Tcp {
if let Ok(tcp_packet) = TcpPacket::new_checked(ipv4_packet.payload()) {
if tcp_packet.syn() && !tcp_packet.ack() {
let dst_ip = ipv4_packet.dst_addr();
let dst_port = tcp_packet.dst_port();
// Allocate a stream_id
let stream_id = stream_id_counter;
stream_id_counter = stream_id_counter.wrapping_add(1);
if stream_id_counter == 0 {
stream_id_counter = 1;
}
let target = format!("{}:{}", dst_ip, dst_port);
debug!("Intercepted TCP SYN to {}, creating virtual socket. Stream ID: {}", target, stream_id);
// Create socket inside smoltcp
let mut tcp_socket = TcpSocket::new(
smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]),
smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]),
);
if let Err(e) = tcp_socket.listen((dst_ip, dst_port)) {
error!("Failed to set TCP socket to listen: {:?}", e);
} else {
let handle = sockets.add(tcp_socket);
streams.insert(stream_id, VirtualStream {
_stream_id: stream_id,
socket_handle: handle,
last_activity: Instant::now(),
target: target.clone(),
established: false,
});
handle_to_stream_id.insert(handle, stream_id);
}
}
}
}
}
device.rx_queue.push_back(pkt);
}
}
msg_opt = client_msgs_rx.recv() => {
if let Some((stream_id, msg)) = msg_opt {
pending_client_msgs.push_back((stream_id, msg));
}
}
}
// Process pending client messages (responses from OSTP bridge)
let mut unhandled = VecDeque::new();
while let Some((stream_id, msg)) = pending_client_msgs.pop_front() {
if let Some(stream) = streams.get_mut(&stream_id) {
let socket = sockets.get_mut::<TcpSocket>(stream.socket_handle);
match msg {
ProxyToClientMsg::ConnectOk => {
stream.established = true;
debug!("Stream ID {} connected successfully via OSTP.", stream_id);
}
ProxyToClientMsg::Data(data) => {
if socket.can_send() {
let _ = socket.send_slice(&data);
stream.last_activity = Instant::now();
}
}
ProxyToClientMsg::Close | ProxyToClientMsg::Error(_) => {
socket.close();
// Socket clean-up will occur below
}
}
} else {
unhandled.push_back((stream_id, msg));
}
}
pending_client_msgs = unhandled;
// Poll the virtual interface
let timestamp = smoltcp::time::Instant::now();
let _ = interface.poll(timestamp, &mut device, &mut sockets);
// Process data transfer from virtual sockets -> OSTP client bridge
let mut closed_streams = Vec::new();
for (&stream_id, stream) in streams.iter_mut() {
let socket = sockets.get_mut::<TcpSocket>(stream.socket_handle);
// 1. Handshake detection & initiation
if socket.is_active() && !stream.established {
if socket.state() == TcpState::Established {
// Send Connect request to the bridge
if proxy_events_tx.try_send(ProxyEvent::NewStream {
stream_id,
target: stream.target.clone(),
}).is_ok() {
stream.established = true;
}
}
}
// 2. Read inbound data from client OS applications
if socket.may_recv() {
let mut buf = vec![0; 4096];
if let Ok(n) = socket.recv_slice(&mut buf) {
if n > 0 {
stream.last_activity = Instant::now();
let _ = proxy_events_tx.try_send(ProxyEvent::Data {
stream_id,
payload: Bytes::from(buf[..n].to_vec()),
});
}
}
}
// 3. Connection termination detection
let mut should_close = false;
if !socket.is_active() || socket.state() == TcpState::Closed || socket.state() == TcpState::TimeWait {
should_close = true;
} else if stream.last_activity.elapsed() > std::time::Duration::from_secs(120) {
// Timeout inactive streams
should_close = true;
socket.abort();
}
if should_close {
closed_streams.push(stream_id);
}
}
// Clean up closed streams
for stream_id in closed_streams {
if let Some(stream) = streams.remove(&stream_id) {
debug!("Cleaning up virtual socket for stream ID: {}", stream_id);
handle_to_stream_id.remove(&stream.socket_handle);
sockets.remove(stream.socket_handle);
let _ = proxy_events_tx.try_send(ProxyEvent::Close { stream_id });
}
}
}
Ok(())
}

View File

@ -1,216 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Result};
use tokio::sync::mpsc;
use tracing::{info, error, debug};
pub struct TunDevice {
pub packet_rx: mpsc::Receiver<Vec<u8>>,
pub packet_tx: mpsc::Sender<Vec<u8>>,
_shutdown_tx: mpsc::Sender<()>,
}
#[cfg(target_os = "windows")]
pub fn create_tun_device(tun_name: &str, mtu: usize) -> Result<TunDevice> {
let exe = std::env::current_exe()?;
let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?;
let wintun_dll = dir.join("wintun.dll");
if !wintun_dll.exists() {
return Err(anyhow!(
"CRITICAL: 'wintun.dll' is missing at {}!\n\
Please make sure wintun.dll is present in the binary directory.",
dir.display()
));
}
info!("Loading wintun.dll from: {:?}", wintun_dll);
let wintun = unsafe { wintun::load_from_path(wintun_dll)? };
// Open or create adapter
let adapter = match wintun::Adapter::open(&wintun, tun_name) {
Ok(a) => a,
Err(_) => {
info!("TUN adapter '{}' not found, creating a new one...", tun_name);
wintun::Adapter::create(&wintun, "Wintun", tun_name, None)?
}
};
let session = Arc::new(adapter.start_session(wintun::MAX_RING_CAPACITY)?);
let (packet_tx_in, packet_rx) = mpsc::channel::<Vec<u8>>(100000);
let (packet_tx, mut packet_rx_out) = mpsc::channel::<Vec<u8>>(100000);
let (shutdown_tx, _shutdown_rx) = mpsc::channel::<()>(1);
// Spawning blocking read loop in a dedicated thread
let session_read = session.clone();
std::thread::spawn(move || {
loop {
match session_read.receive_blocking() {
Ok(packet) => {
let bytes: &[u8] = packet.bytes();
if packet_tx_in.blocking_send(bytes.to_vec()).is_err() {
break;
}
}
Err(e) => {
error!("Wintun receive packet error: {:?}", e);
break;
}
}
}
});
// Spawning blocking write loop in a dedicated thread
let session_write = session.clone();
std::thread::spawn(move || {
while let Some(pkt) = packet_rx_out.blocking_recv() {
if pkt.len() > mtu {
debug!("Dropped packet exceeding MTU: {} > {}", pkt.len(), mtu);
continue;
}
match session_write.allocate_send_packet(pkt.len() as u16) {
Ok(mut send_packet) => {
send_packet.bytes_mut().copy_from_slice(&pkt);
session_write.send_packet(send_packet);
}
Err(e) => {
error!("Wintun allocate send packet error: {:?}", e);
}
}
}
});
Ok(TunDevice {
packet_rx,
packet_tx,
_shutdown_tx: shutdown_tx,
})
}
#[cfg(target_os = "linux")]
pub fn create_tun_device(tun_name: &str, mtu: usize) -> Result<TunDevice> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut config = tun::Configuration::default();
config
.name(tun_name)
.address("10.1.0.2")
.netmask("255.255.255.0")
.mtu(mtu as i32)
.up();
let device = tun::create_as_async(&config)?;
let (mut reader, mut writer) = tokio::io::split(device);
let (packet_tx_in, packet_rx) = mpsc::channel::<Vec<u8>>(100000);
let (packet_tx, mut packet_rx_out) = mpsc::channel::<Vec<u8>>(100000);
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
// Read loop
tokio::spawn(async move {
let mut buf = vec![0_u8; 65535];
loop {
match reader.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if packet_tx_in.send(buf[..n].to_vec()).await.is_err() {
break;
}
}
Err(e) => {
error!("TUN read error: {:?}", e);
break;
}
}
}
});
// Write loop
tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
break;
}
pkt_opt = packet_rx_out.recv() => {
if let Some(pkt) = pkt_opt {
if let Err(e) = writer.write_all(&pkt).await {
error!("TUN write error: {:?}", e);
break;
}
} else {
break;
}
}
}
}
});
Ok(TunDevice {
packet_rx,
packet_tx,
_shutdown_tx: shutdown_tx,
})
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
pub fn create_tun_device(_tun_name: &str, _mtu: usize) -> Result<TunDevice> {
Err(anyhow!("Unsupported operating system for TUN device"))
}
#[cfg(unix)]
pub fn create_tun_device_from_fd(fd: i32, mtu: usize) -> Result<TunDevice> {
use std::os::unix::io::FromRawFd;
use std::io::{Read, Write};
let file = unsafe { std::fs::File::from_raw_fd(fd) };
let mut file_read = file.try_clone()?;
let mut file_write = file;
let (packet_tx_in, packet_rx) = mpsc::channel::<Vec<u8>>(100000);
let (packet_tx, mut packet_rx_out) = mpsc::channel::<Vec<u8>>(100000);
let (shutdown_tx, _shutdown_rx) = mpsc::channel::<()>(1);
// Read loop thread
std::thread::spawn(move || {
let mut buf = vec![0_u8; 65535];
loop {
match file_read.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if packet_tx_in.blocking_send(buf[..n].to_vec()).is_err() {
break;
}
}
Err(e) => {
error!("TUN fd read error: {:?}", e);
break;
}
}
}
});
// Write loop thread
std::thread::spawn(move || {
while let Some(pkt) = packet_rx_out.blocking_recv() {
if pkt.len() > mtu {
continue;
}
if let Err(e) = file_write.write_all(&pkt) {
error!("TUN fd write error: {:?}", e);
break;
}
}
});
Ok(TunDevice {
packet_rx,
packet_tx,
_shutdown_tx: shutdown_tx,
})
}
#[cfg(not(unix))]
pub fn create_tun_device_from_fd(_fd: i32, _mtu: usize) -> Result<TunDevice> {
Err(anyhow!("Raw fd TUN device is not supported on this operating system"))
}

View File

@ -1,36 +1,29 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use tokio::sync::{mpsc, watch}; use tokio::sync::watch;
#[cfg(target_os = "windows")]
use tracing::info;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
#[cfg(target_os = "windows")]
use crate::tunnel::tun_device::create_tun_device;
#[cfg(target_os = "windows")]
use crate::tunnel::smoltcp_stack::run_smoltcp_stack;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn run_wintun_tunnel( pub async fn run_wintun_tunnel(
config: crate::config::ClientConfig, config: crate::config::ClientConfig,
proxy_events_tx: mpsc::Sender<ProxyEvent>,
client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use std::process::Command; use std::process::{Command, Stdio, Child};
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
const TUN_NAME: &str = "ostp_tun"; const TUN_NAME: &str = "ostp_tun";
struct WintunGuard { struct WintunGuard {
server_ip_str: String, server_ip_str: String,
child: Option<Child>,
} }
impl Drop for WintunGuard { impl Drop for WintunGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
let cleanup_script = format!( let cleanup_script = format!(
"$remote_ip = '{}'\n\ "$remote_ip = '{}'\n\
Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\ Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\
@ -46,9 +39,27 @@ pub async fn run_wintun_tunnel(
} }
} }
info!("Initializing built-in Wintun TUN tunnel..."); let debug = config.debug;
tracing::info!("Initializing TUN tunnel...");
let exe = std::env::current_exe()?;
let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?;
let tun2socks_exe = dir.join("tun2socks.exe");
if !tun2socks_exe.exists() {
return Err(anyhow!(
"CRITICAL: 'tun2socks.exe' binary is missing!\n\
OSTP requires tun2socks for TUN mode on Windows. Please download the appropriate binary from: \n\
https://github.com/xjasonlyu/tun2socks/releases \n\
and place it in the same directory as the ostp executable ({}).",
dir.display()
));
}
// 1. Delete stale TUN adapter if it exists from a previous run. // 1. Delete stale TUN adapter if it exists from a previous run.
// This prevents wintun from creating "ostp_tun 2", "ostp_tun 3", etc.
tracing::info!("Cleaning up stale TUN adapter...");
let _ = Command::new("powershell") let _ = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW) .creation_flags(CREATE_NO_WINDOW)
.args(["-NoProfile", "-Command", &format!( .args(["-NoProfile", "-Command", &format!(
@ -57,6 +68,7 @@ pub async fn run_wintun_tunnel(
netsh interface set interface \"{TUN_NAME}\" admin=disable 2>$null" netsh interface set interface \"{TUN_NAME}\" admin=disable 2>$null"
)]) )])
.output(); .output();
// Brief pause to let the driver release the adapter
tokio::time::sleep(std::time::Duration::from_millis(200)).await; tokio::time::sleep(std::time::Duration::from_millis(200)).await;
// 2. Resolve Server IP for routing table exclusion // 2. Resolve Server IP for routing table exclusion
@ -67,7 +79,7 @@ pub async fn run_wintun_tunnel(
.ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?; .ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?;
let server_ip_str = server_ip.to_string(); let server_ip_str = server_ip.to_string();
info!("Resolved server IP: {}", server_ip_str); tracing::info!("Resolved server IP: {}", server_ip_str);
// 3. Prepare routing and firewall setup script // 3. Prepare routing and firewall setup script
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned(); let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
@ -75,33 +87,42 @@ pub async fn run_wintun_tunnel(
let setup_script = format!( let setup_script = format!(
"$remote_ip = '{}'\n\ "$remote_ip = '{}'\n\
$exe_path = '{}'\n\ $exe_path = '{}'\n\
$route = Find-NetRoute -RemoteIPAddress $remote_ip -ErrorAction SilentlyContinue | Select-Object -First 1\n\
if (-not $route) {{\n\
$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {{ $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' }} | Sort-Object RouteMetric | Select-Object -First 1\n\ $route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {{ $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' }} | Sort-Object RouteMetric | Select-Object -First 1\n\
}}\n\ if ($route) {{\n\
$gw = $route.NextHop\n\ $gw = $route.NextHop\n\
$ifIndex = $route.InterfaceIndex\n\ $ifIndex = $route.InterfaceIndex\n\
New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\ New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
$dns_ips = Get-DnsClientServerAddress -InterfaceIndex $ifIndex | Select-Object -ExpandProperty ServerAddresses\n\
foreach ($dns in $dns_ips) {{\n\
if ($dns -match '^\\d+\\.\\d+\\.\\d+\\.\\d+$') {{\n\
New-NetRoute -DestinationPrefix \"$dns/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
}}\n\ }}\n\
}}\n\
New-NetRoute -DestinationPrefix \"1.1.1.1/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
New-NetFirewallRule -DisplayName 'OSTP Tunnel In' -Direction Inbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\ New-NetFirewallRule -DisplayName 'OSTP Tunnel In' -Direction Inbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\
New-NetFirewallRule -DisplayName 'OSTP Tunnel Out' -Direction Outbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n", New-NetFirewallRule -DisplayName 'OSTP Tunnel Out' -Direction Outbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n",
server_ip_str, current_exe server_ip_str, current_exe
); );
// Create the TunDevice inside the client process // 4. Launch tun2socks + route setup IN PARALLEL to save ~3 seconds
let tun_dev = create_tun_device(TUN_NAME, config.ostp.mtu)?; let proxy_url = format!("http://{}", config.local_proxy.bind_addr);
tracing::info!("Starting tun2socks (proxy={})", proxy_url);
// Spawn tun2socks immediately — it creates the adapter on its own
let mut child = Command::new(&tun2socks_exe)
.creation_flags(CREATE_NO_WINDOW)
.args([
"-device", TUN_NAME,
"-proxy", &proxy_url,
"-loglevel", if debug { "debug" } else { "error" }
])
.current_dir(dir)
.stdout(if debug { Stdio::piped() } else { Stdio::null() })
.stderr(if debug { Stdio::piped() } else { Stdio::null() })
.spawn()
.map_err(|e| anyhow!("Failed to launch tun2socks.exe: {}", e))?;
let mut _guard = WintunGuard { let mut _guard = WintunGuard {
server_ip_str: server_ip_str.clone(), server_ip_str: server_ip_str.clone(),
child: None,
}; };
// Run route setup in parallel // Run route setup in parallel while tun2socks creates the adapter.
// Also poll for the adapter to appear (typically <1s).
let route_handle = { let route_handle = {
let script = setup_script.clone(); let script = setup_script.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@ -112,7 +133,7 @@ pub async fn run_wintun_tunnel(
}) })
}; };
// 5. Wait for TUN adapter to appear // 5. Wait for TUN adapter to appear (poll with timeout instead of fixed 2s sleep)
let adapter_deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(8); let adapter_deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(8);
let mut adapter_ready = false; let mut adapter_ready = false;
while tokio::time::Instant::now() < adapter_deadline { while tokio::time::Instant::now() < adapter_deadline {
@ -124,6 +145,9 @@ pub async fn run_wintun_tunnel(
.output(); .output();
if let Ok(out) = check { if let Ok(out) = check {
let status = String::from_utf8_lossy(&out.stdout).trim().to_string(); let status = String::from_utf8_lossy(&out.stdout).trim().to_string();
if debug {
tracing::info!("Adapter status: '{}'", status);
}
if status == "Up" || status == "Disconnected" || !status.is_empty() { if status == "Up" || status == "Disconnected" || !status.is_empty() {
adapter_ready = true; adapter_ready = true;
break; break;
@ -135,21 +159,22 @@ pub async fn run_wintun_tunnel(
tracing::warn!("WARNING: TUN adapter did not appear within timeout. Proceeding anyway."); tracing::warn!("WARNING: TUN adapter did not appear within timeout. Proceeding anyway.");
} }
// Wait for route setup to finish // Wait for route setup to finish (should already be done by now)
let _ = route_handle.await; let _ = route_handle.await;
// 6. Configure the adapter // 6. Configure the adapter (IP, metric, MTU, DNS)
info!("Applying network configuration..."); tracing::info!("Applying network configuration...");
let mut net_setup = format!( let mut net_setup = format!(
"netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\ "netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\
netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\ netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\
netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n", netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=1\n\
New-NetRoute -DestinationPrefix '0.0.0.0/0' -InterfaceAlias '{TUN_NAME}' -NextHop '10.1.0.1' -RouteMetric 1 -ErrorAction SilentlyContinue\n",
config.ostp.mtu config.ostp.mtu
); );
if let Some(ref dns) = config.dns_server { if let Some(ref dns) = config.dns_server {
if !dns.is_empty() { if !dns.is_empty() {
info!("DNS server: {}", dns); tracing::info!("DNS server: {}", dns);
net_setup.push_str(&format!( net_setup.push_str(&format!(
"netsh interface ipv4 set dnsservers name=\"{TUN_NAME}\" static {} primary\n", dns "netsh interface ipv4 set dnsservers name=\"{TUN_NAME}\" static {} primary\n", dns
)); ));
@ -161,33 +186,41 @@ pub async fn run_wintun_tunnel(
.args(["-NoProfile", "-Command", &net_setup]) .args(["-NoProfile", "-Command", &net_setup])
.output()?; .output()?;
info!("TUN tunnel active. Direct in-process packets handling started."); tracing::info!("TUN tunnel active. All traffic is routed through OSTP.");
// Run the smoltcp stack loop in the background // 7. Spawn debug log readers for tun2socks output
let stack_shutdown_rx = shutdown.clone(); let mut stdout = child.stdout.take();
let stack_handle = tokio::spawn(async move { let mut stderr = child.stderr.take();
if let Err(e) = run_smoltcp_stack( _guard.child = Some(child);
tun_dev.packet_rx,
tun_dev.packet_tx, if debug {
config.ostp.mtu, std::thread::spawn(move || {
proxy_events_tx, use std::io::{BufRead, BufReader};
client_msgs_rx, if let Some(out) = stdout.take() {
stack_shutdown_rx, let reader = BufReader::new(out);
).await { for line in reader.lines().map_while(Result::ok) {
tracing::error!("smoltcp stack loop failed: {:?}", e); tracing::debug!("tun2socks: {}", line);
}
} }
}); });
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
if let Some(err) = stderr.take() {
let reader = BufReader::new(err);
for line in reader.lines().map_while(Result::ok) {
tracing::warn!("tun2socks: {}", line);
}
}
});
}
// 8. Wait for shutdown signal // 8. Wait for shutdown signal
let _ = shutdown.changed().await; let _ = shutdown.changed().await;
info!("Deactivating TUN tunnel..."); tracing::info!("Deactivating TUN tunnel...");
drop(_guard); drop(_guard);
tracing::info!("TUN tunnel stopped.");
// Terminate smoltcp stack
let _ = stack_handle.await;
info!("TUN tunnel stopped.");
Ok(()) Ok(())
} }
@ -195,8 +228,6 @@ pub async fn run_wintun_tunnel(
#[allow(dead_code)] #[allow(dead_code)]
pub async fn run_wintun_tunnel( pub async fn run_wintun_tunnel(
_config: crate::config::ClientConfig, _config: crate::config::ClientConfig,
_proxy_events_tx: mpsc::Sender<ProxyEvent>,
_client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>,
_shutdown: watch::Receiver<bool>, _shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
Err(anyhow!("Wintun driver executed on a non-Windows host!")) Err(anyhow!("Wintun driver executed on a non-Windows host!"))

View File

@ -165,16 +165,8 @@ impl CongestionController {
} }
} }
Phase::ProbeBandwidth => { Phase::ProbeBandwidth => {
// BBR-style: target cwnd = BDP * gain // TCP Reno Additive Increase: increase cwnd by ~1 MTU per RTT
let bdp = self.bandwidth_delay_product();
// Apply gain of 1.25 during probe bandwidth
let target = (bdp * 5 / 4).max(MIN_CWND_PACKETS * self.mtu);
// Smooth transition
if self.cwnd < target {
self.cwnd = self.cwnd.saturating_add(bytes * self.mtu / self.cwnd.max(1)); self.cwnd = self.cwnd.saturating_add(bytes * self.mtu / self.cwnd.max(1));
} else {
self.cwnd = target;
}
} }
Phase::ProbeRtt => { Phase::ProbeRtt => {
// Drain down to 4 packets to measure true min RTT // Drain down to 4 packets to measure true min RTT
@ -184,20 +176,21 @@ impl CongestionController {
// ProbeRTT complete, return to ProbeBandwidth // ProbeRTT complete, return to ProbeBandwidth
self.phase = Phase::ProbeBandwidth; self.phase = Phase::ProbeBandwidth;
self.probe_rtt_timer = None; self.probe_rtt_timer = None;
let bdp = self.bandwidth_delay_product(); self.cwnd = (MIN_CWND_PACKETS * self.mtu * 4).max(self.cwnd);
self.cwnd = bdp.max(MIN_CWND_PACKETS * self.mtu);
tracing::debug!(cwnd = self.cwnd, min_rtt = ?self.min_rtt, "congestion: probe RTT complete"); tracing::debug!(cwnd = self.cwnd, min_rtt = ?self.min_rtt, "congestion: probe RTT complete");
} }
} }
} }
} }
/*
// Periodically enter ProbeRTT to refresh min_rtt // Periodically enter ProbeRTT to refresh min_rtt
if now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY && self.phase != Phase::ProbeRtt { if now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY && self.phase != Phase::ProbeRtt {
self.phase = Phase::ProbeRtt; self.phase = Phase::ProbeRtt;
self.probe_rtt_timer = Some(now); self.probe_rtt_timer = Some(now);
tracing::debug!("congestion: entering probe RTT phase"); tracing::debug!("congestion: entering probe RTT phase");
} }
*/
self.update_pacing_rate(); self.update_pacing_rate();
self.last_ack_time = now; self.last_ack_time = now;

View File

@ -100,6 +100,7 @@ pub struct ProtocolMachine {
/// Key-derived handshake padding range /// Key-derived handshake padding range
handshake_pad_min: usize, handshake_pad_min: usize,
handshake_pad_max: usize, handshake_pad_max: usize,
mtu: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -145,6 +146,7 @@ impl ProtocolMachine {
cc: CongestionController::new(config.mtu as u64), cc: CongestionController::new(config.mtu as u64),
handshake_pad_min: config.handshake_pad_min.max(8), handshake_pad_min: config.handshake_pad_min.max(8),
handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16), handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16),
mtu: config.mtu,
}) })
} }

View File

@ -42,6 +42,7 @@ struct TunConfig {
wintun_path: Option<String>, wintun_path: Option<String>,
ipv4_address: Option<String>, ipv4_address: Option<String>,
dns: Option<String>, dns: Option<String>,
stack: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -179,6 +180,7 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi
sessions: raw.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1), sessions: raw.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
}, },
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()), dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()),
} }
} }

View File

@ -16,7 +16,6 @@
<div class="ambient" aria-hidden="true"> <div class="ambient" aria-hidden="true">
<div class="blob blob-1"></div> <div class="blob blob-1"></div>
<div class="blob blob-2"></div> <div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div> </div>
<!-- ── HOME SCREEN ──────────────────────────────────────────── --> <!-- ── HOME SCREEN ──────────────────────────────────────────── -->
@ -29,9 +28,6 @@
<span class="brand-name">OSTP</span> <span class="brand-name">OSTP</span>
</div> </div>
<div class="topbar-right"> <div class="topbar-right">
<button id="btn-lang" class="pill-btn" title="Language">
<span id="lang-label">EN</span>
</button>
<button id="btn-go-settings" class="icon-btn" aria-label="Settings"> <button id="btn-go-settings" class="icon-btn" aria-label="Settings">
<!-- Gear icon --> <!-- Gear icon -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -49,7 +45,6 @@
<div class="orbit-wrap" id="orbit-wrap"> <div class="orbit-wrap" id="orbit-wrap">
<div class="orbit orbit-1"></div> <div class="orbit orbit-1"></div>
<div class="orbit orbit-2"></div> <div class="orbit orbit-2"></div>
<div class="orbit orbit-3"></div>
<!-- Power button --> <!-- Power button -->
<button id="btn-connect" class="power-btn" aria-label="Connect"> <button id="btn-connect" class="power-btn" aria-label="Connect">
@ -68,9 +63,10 @@
<div id="uptime-text" class="status-sub" data-i18n="hint_tap">Tap to protect your traffic</div> <div id="uptime-text" class="status-sub" data-i18n="hint_tap">Tap to protect your traffic</div>
</div> </div>
<!-- Server badge (shown when connected) --> <!-- Connection info (shown when connected) -->
<div id="server-badge" class="server-badge hidden"> <div id="connection-info" class="connection-info hidden">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <div class="server-badge">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2"/> <rect x="2" y="2" width="20" height="8" rx="2"/>
<rect x="2" y="14" width="20" height="8" rx="2"/> <rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/> <line x1="6" y1="6" x2="6.01" y2="6"/>
@ -79,6 +75,20 @@
<span id="server-badge-text"></span> <span id="server-badge-text"></span>
</div> </div>
<div class="ping-test-box">
<div class="ping-test-left">
<span class="ping-test-title">CONNECTION TEST</span>
<span id="ping-text-value" class="ping-test-value">Target Ping: -- ms</span>
</div>
<button id="btn-test-ping" class="ping-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
<span>Test Ping</span>
</button>
</div>
</div>
</main> </main>
<!-- Traffic metrics bar --> <!-- Traffic metrics bar -->
@ -108,35 +118,6 @@
<span id="metric-up" class="metric-value">0 B</span> <span id="metric-up" class="metric-value">0 B</span>
</div> </div>
</div> </div>
<div class="metric-sep"></div>
<div class="metric">
<div class="metric-icon ping-icon" id="ping-icon-block">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<div class="metric-body">
<span class="metric-label">Ping</span>
<span id="metric-ping" class="metric-value">— ms</span>
</div>
</div>
<div class="metric-sep"></div>
<div class="metric">
<div class="metric-icon mode-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2"/>
<path d="M8 21h8M12 17v4"/>
</svg>
</div>
<div class="metric-body">
<span class="metric-label">Mode</span>
<span id="metric-mode" class="metric-value"></span>
</div>
</div>
</footer> </footer>
</div> </div>
@ -243,6 +224,14 @@
</label> </label>
</div> </div>
<div class="field-group" id="group-tun-stack">
<label class="field-label" for="in-tun-stack" data-i18n="label_tun_stack">TUN Stack</label>
<select id="in-tun-stack" class="field-input">
<option value="system">System (tun2socks)</option>
<option value="ostp">OSTP (Native)</option>
</select>
</div>
<div class="toggle-row"> <div class="toggle-row">
<div class="toggle-text"> <div class="toggle-text">
<span class="toggle-name" data-i18n="label_mux">Multiplexing (Mux)</span> <span class="toggle-name" data-i18n="label_mux">Multiplexing (Mux)</span>
@ -291,8 +280,7 @@
</div> </div>
</div> </div>
<!-- Save -->
<button id="btn-save-config" class="save-btn" data-i18n="save_btn">Save &amp; Apply</button>
</div> </div>
</div> </div>

View File

@ -24,21 +24,18 @@ const orbitWrap = $('orbit-wrap');
const brandDot = $('brand-dot'); const brandDot = $('brand-dot');
const statusLabel = $('status-text'); const statusLabel = $('status-text');
const statusSub = $('uptime-text'); const statusSub = $('uptime-text');
const serverBadge = $('server-badge'); const connInfo = $('connection-info');
const serverBadgeTxt = $('server-badge-text'); const serverBadgeTxt = $('server-badge-text');
const metricDown = $('metric-down'); const metricDown = $('metric-down');
const metricUp = $('metric-up'); const metricUp = $('metric-up');
const metricMode = $('metric-mode'); const pingValueTxt = $('ping-text-value');
const metricPing = $('metric-ping'); const btnTestPing = $('btn-test-ping');
const pingIconBlock = $('ping-icon-block');
const toast = $('toast'); const toast = $('toast');
const btnGoSettings = $('btn-go-settings'); const btnGoSettings = $('btn-go-settings');
const btnBack = $('btn-back'); const btnBack = $('btn-back');
const btnLang = $('btn-lang');
const btnImport = $('btn-import-url'); const btnImport = $('btn-import-url');
const btnPeekKey = $('btn-peek-key'); const btnPeekKey = $('btn-peek-key');
const btnSave = $('btn-save-config');
const importInput = $('in-import-url'); const importInput = $('in-import-url');
const inServer = $('in-server'); const inServer = $('in-server');
const inKey = $('in-key'); const inKey = $('in-key');
@ -50,6 +47,8 @@ const inPbk = $('in-pbk');
const inSid = $('in-sid'); const inSid = $('in-sid');
const inMtu = $('in-mtu'); const inMtu = $('in-mtu');
const inTun = $('in-tun-mode'); const inTun = $('in-tun-mode');
const inTunStack = $('in-tun-stack');
const groupTunStack = $('group-tun-stack');
const inMux = $('in-mux-mode'); const inMux = $('in-mux-mode');
const inMuxSessions = $('in-mux-sessions'); const inMuxSessions = $('in-mux-sessions');
const inDebug = $('in-debug'); const inDebug = $('in-debug');
@ -105,10 +104,11 @@ function setState(next) {
statusLabel.textContent = t('status_disconnected'); statusLabel.textContent = t('status_disconnected');
statusSub.textContent = t('hint_tap'); statusSub.textContent = t('hint_tap');
statusLabel.classList.add(''); statusLabel.classList.add('');
serverBadge.classList.add('hidden'); connInfo.classList.add('hidden');
metricDown.textContent = '0 B'; metricDown.textContent = '0 B';
metricUp.textContent = '0 B'; metricUp.textContent = '0 B';
metricMode.textContent = '—'; pingValueTxt.textContent = 'Target Ping: -- ms';
pingValueTxt.className = 'ping-test-value';
clearInterval(pollTimer); clearInterval(pollTimer);
clearInterval(uptimeTimer); clearInterval(uptimeTimer);
pollTimer = uptimeTimer = null; pollTimer = uptimeTimer = null;
@ -121,7 +121,7 @@ function setState(next) {
statusLabel.classList.add('is-connecting'); statusLabel.classList.add('is-connecting');
statusLabel.textContent = t('status_connecting'); statusLabel.textContent = t('status_connecting');
statusSub.textContent = t('hint_connecting'); statusSub.textContent = t('hint_connecting');
serverBadge.classList.add('hidden'); connInfo.classList.add('hidden');
clearInterval(uptimeTimer); clearInterval(uptimeTimer);
uptimeSecs = 0; uptimeSecs = 0;
@ -132,10 +132,10 @@ function setState(next) {
statusLabel.classList.add('is-connected'); statusLabel.classList.add('is-connected');
statusLabel.textContent = t('status_connected'); statusLabel.textContent = t('status_connected');
// Show server badge // Show connection info
if (serverAddr) { if (serverAddr) {
serverBadgeTxt.textContent = serverAddr; serverBadgeTxt.textContent = serverAddr;
serverBadge.classList.remove('hidden'); connInfo.classList.remove('hidden');
} }
// Start uptime counter // Start uptime counter
@ -161,29 +161,6 @@ async function poll() {
if (metrics) { if (metrics) {
metricDown.textContent = fmtBytes(metrics.bytes_recv); metricDown.textContent = fmtBytes(metrics.bytes_recv);
metricUp.textContent = fmtBytes(metrics.bytes_sent); metricUp.textContent = fmtBytes(metrics.bytes_sent);
const isTun = rawConfig?.tun?.enable;
metricMode.textContent = isTun ? 'TUN' : 'SOCKS5';
const rtt = metrics.rtt_ms || 0;
if (rtt > 0) {
metricPing.textContent = rtt + ' ms';
// Color code: green < 80ms, yellow < 200ms, red >= 200ms
if (rtt < 80) {
metricPing.style.color = 'var(--ping-good, #4ade80)';
pingIconBlock.style.color = 'var(--ping-good, #4ade80)';
} else if (rtt < 200) {
metricPing.style.color = 'var(--ping-mid, #facc15)';
pingIconBlock.style.color = 'var(--ping-mid, #facc15)';
} else {
metricPing.style.color = 'var(--ping-bad, #f87171)';
pingIconBlock.style.color = 'var(--ping-bad, #f87171)';
}
} else {
metricPing.textContent = '— ms';
metricPing.style.color = '';
pingIconBlock.style.color = '';
}
} }
} catch { } catch {
setState('disconnected'); setState('disconnected');
@ -199,15 +176,10 @@ function startPolling() {
// ── Connect / Disconnect ───────────────────────────────────────────────────── // ── Connect / Disconnect ─────────────────────────────────────────────────────
async function handleToggle() { async function handleToggle() {
if (appState === 'disconnected') { if (appState === 'disconnected') {
// Read server address for badge before connecting
try { try {
const raw = await invoke('get_config'); const raw = await invoke('get_config');
const cfg = JSON.parse(raw); const cfg = JSON.parse(raw);
serverAddr = cfg.server || ''; serverAddr = cfg.server || '';
// Determine mode label
const isTun = cfg.tun?.enable;
metricMode.textContent = isTun ? 'TUN' : 'SOCKS5';
} catch { serverAddr = ''; } } catch { serverAddr = ''; }
setState('connecting'); setState('connecting');
@ -260,8 +232,11 @@ async function loadConfigIntoForm() {
inSid.value = c.reality?.sid || ''; inSid.value = c.reality?.sid || '';
inMtu.value = c.mtu || ''; inMtu.value = c.mtu || '';
inTun.checked = !!c.tun?.enable; inTun.checked = !!c.tun?.enable;
inTunStack.value = c.tun?.stack || 'system';
inMux.checked = !!c.mux?.enabled; inMux.checked = !!c.mux?.enabled;
inMuxSessions.value = c.mux?.sessions || ''; inMuxSessions.value = c.mux?.sessions || '';
groupTunStack.style.display = inTun.checked ? 'block' : 'none';
inDns.value = c.tun?.dns || ''; inDns.value = c.tun?.dns || '';
inDebug.checked = !!c.debug; inDebug.checked = !!c.debug;
@ -275,14 +250,20 @@ async function loadConfigIntoForm() {
} }
// ── Config — save ───────────────────────────────────────────────────────────── // ── Config — save ─────────────────────────────────────────────────────────────
async function handleSave() { let autoSaveTimer = null;
function scheduleAutoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => handleSave(true), 600);
}
async function handleSave(silent = false) {
if (!rawConfig) rawConfig = { mode: 'client', log_level: 'info' }; if (!rawConfig) rawConfig = { mode: 'client', log_level: 'info' };
const server = inServer.value.trim(); const server = inServer.value.trim();
const key = inKey.value.trim(); const key = inKey.value.trim();
if (!server) { showToast(t('err_server_req') || 'Server address required', 'error'); return; } if (!server) { if (!silent) showToast(t('err_server_req') || 'Server address required', 'error'); return; }
if (!key) { showToast(t('err_key_req') || 'Access key required', 'error'); return; } if (!key) { if (!silent) showToast(t('err_key_req') || 'Access key required', 'error'); return; }
rawConfig.mode = 'client'; rawConfig.mode = 'client';
rawConfig.server = server; rawConfig.server = server;
@ -324,6 +305,7 @@ async function handleSave() {
} }
rawConfig.tun.enable = inTun.checked; rawConfig.tun.enable = inTun.checked;
rawConfig.tun.dns = inDns.value.trim() || null; rawConfig.tun.dns = inDns.value.trim() || null;
rawConfig.tun.stack = inTunStack.value;
rawConfig.exclude = { rawConfig.exclude = {
domains: splitLines(inDomains.value), domains: splitLines(inDomains.value),
@ -333,14 +315,11 @@ async function handleSave() {
try { try {
const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) }); const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
if (ok) { if (!ok && !silent) {
showToast(t('toast_saved'), 'ok');
setTimeout(() => showScreen('home'), 700);
} else {
showToast(t('toast_error'), 'error'); showToast(t('toast_error'), 'error');
} }
} catch (err) { } catch (err) {
showToast(String(err), 'error'); if (!silent) showToast(String(err), 'error');
} }
} }
@ -366,6 +345,7 @@ function handleImport() {
importInput.value = ''; importInput.value = '';
showToast(t('toast_imported'), 'ok'); showToast(t('toast_imported'), 'ok');
handleSave(false);
} catch (err) { } catch (err) {
showToast(err.message, 'error'); showToast(err.message, 'error');
} }
@ -390,19 +370,35 @@ window.addEventListener('DOMContentLoaded', async () => {
btnConnect.addEventListener('click', handleToggle); btnConnect.addEventListener('click', handleToggle);
btnGoSettings.addEventListener('click', () => showScreen('settings')); btnGoSettings.addEventListener('click', () => showScreen('settings'));
btnBack.addEventListener('click', () => showScreen('home')); btnBack.addEventListener('click', () => showScreen('home'));
btnSave.addEventListener('click', handleSave);
btnImport.addEventListener('click', handleImport); btnImport.addEventListener('click', handleImport);
btnPeekKey.addEventListener('click', togglePeek); btnPeekKey.addEventListener('click', togglePeek);
inTun.addEventListener('change', () => { groupTunStack.style.display = inTun.checked ? 'block' : 'none'; });
importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); }); importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); });
btnLang.addEventListener('click', () => { // Auto-save wiring
toggleLang(); const formInputs = document.querySelectorAll('#settings-screen input:not(#in-import-url), #settings-screen textarea, #settings-screen select');
// Refresh dynamic text without losing state formInputs.forEach(el => {
const cur = appState; el.addEventListener('input', scheduleAutoSave);
appState = ''; el.addEventListener('change', scheduleAutoSave);
setState(cur); });
document.getElementById('lang-label').textContent =
localStorage.getItem('ostp_lang') === 'ru' ? 'RU' : 'EN'; btnTestPing.addEventListener('click', async () => {
pingValueTxt.textContent = 'Testing...';
pingValueTxt.className = 'ping-test-value';
try {
const metrics = await invoke('get_metrics');
if (metrics && metrics.rtt_ms > 0) {
const rtt = metrics.rtt_ms;
pingValueTxt.textContent = `Target Ping: ${rtt} ms`;
if (rtt < 80) pingValueTxt.classList.add('good');
else if (rtt < 200) pingValueTxt.classList.add('warn');
else pingValueTxt.classList.add('bad');
} else {
pingValueTxt.textContent = 'Target Ping: -- ms';
}
} catch {
pingValueTxt.textContent = 'Target Ping: Error';
}
}); });
// Restore status on app open // Restore status on app open

View File

@ -84,35 +84,26 @@ input, textarea { font-family: inherit; }
.blob { .blob {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(80px); filter: blur(100px);
will-change: transform; will-change: transform;
} }
.blob-1 { .blob-1 {
width: 360px; height: 360px; width: 400px; height: 400px;
background: var(--c-accent); background: var(--c-accent);
opacity: 0.055; opacity: 0.15;
top: -140px; right: -80px; top: -150px; right: -100px;
animation: blob-drift 28s infinite alternate ease-in-out; animation: blob-drift 28s infinite alternate ease-in-out;
} }
.blob-2 { .blob-2 {
width: 280px; height: 280px; width: 350px; height: 350px;
background: var(--c-green); background: var(--c-green);
opacity: 0.04; opacity: 0.10;
bottom: -100px; left: -60px; bottom: -100px; left: -100px;
animation: blob-drift 22s infinite alternate-reverse ease-in-out; animation: blob-drift 22s infinite alternate-reverse ease-in-out;
} }
.blob-3 {
width: 200px; height: 200px;
background: var(--c-accent-2);
opacity: 0.035;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
animation: blob-drift 35s infinite alternate ease-in-out;
}
@keyframes blob-drift { @keyframes blob-drift {
from { transform: translate(0, 0); } from { transform: translate(0, 0); }
to { transform: translate(30px, 20px); } to { transform: translate(30px, 20px); }
@ -379,38 +370,100 @@ input, textarea { font-family: inherit; }
opacity: 0.7; opacity: 0.7;
} }
/* ── Server badge ─────────────────────────────────────────────────────────── */ /* ── Connection info ──────────────────────────────────────────────────────── */
.connection-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
transition: all var(--t-med);
}
.connection-info.hidden {
opacity: 0; pointer-events: none; transform: translateY(4px);
}
.server-badge { .server-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border-radius: 30px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
font-size: 0.95rem;
color: rgba(255,255,255,0.7);
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
backdrop-filter: blur(8px);
}
.ping-test-box {
display: flex;
justify-content: space-between;
align-items: center;
width: calc(100% - 64px);
max-width: 400px;
padding: 12px 16px;
background: rgba(255,255,255,0.03);
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.06);
}
.ping-test-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.ping-test-title {
font-size: 0.62rem;
font-weight: 700;
color: rgba(255,255,255,0.38);
letter-spacing: 0.8px;
}
.ping-test-value {
font-size: 0.9rem;
font-weight: 700;
color: rgba(255,255,255,0.7);
transition: color var(--t-fast);
}
.ping-test-value.good { color: var(--c-green); }
.ping-test-value.warn { color: var(--c-amber); }
.ping-test-value.bad { color: var(--c-red); }
.ping-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 5px 12px; background: var(--c-accent-dim);
border-radius: var(--r-full); border: none;
background: var(--c-card); padding: 8px 12px;
border: 1px solid var(--c-card-border); border-radius: 12px;
font-size: 0.7rem; color: var(--c-accent);
color: var(--c-txt-2); font-size: 0.8rem;
font-family: 'JetBrains Mono', monospace; font-weight: 700;
backdrop-filter: blur(8px); transition: all var(--t-fast);
transition: all var(--t-med);
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.server-badge.hidden { opacity: 0; pointer-events: none; transform: translateY(4px); } .ping-btn:hover {
background: rgba(108,114,255,0.2);
transform: scale(1.03);
}
.ping-btn:active { transform: scale(0.96); }
/* ── Metrics bar ──────────────────────────────────────────────────────────── */ /* ── Metrics bar ──────────────────────────────────────────────────────────── */
.metrics-bar { .metrics-bar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0; gap: 0;
padding: 12px 4px 16px; padding: 24px 20px;
flex-shrink: 0; flex-shrink: 0;
background: rgba(255,255,255,0.02); background: rgba(255,255,255,0.04);
border-top: 1px solid var(--c-card-border); border-top: 1px solid rgba(255,255,255,0.08);
border-radius: var(--r-lg) var(--r-lg) 0 0;
margin: 0 -18px; margin: 0 -18px;
padding-inline: 18px; padding-inline: 18px;
} }
@ -419,27 +472,27 @@ input, textarea { font-family: inherit; }
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: center;
gap: 12px;
} }
.metric-sep { .metric-sep {
width: 1px; width: 1px;
height: 28px; height: 40px;
background: var(--c-card-border); background: rgba(255,255,255,0.15);
flex-shrink: 0; flex-shrink: 0;
margin: 0 4px; margin: 0 4px;
} }
.metric-icon { .metric-icon {
width: 26px; height: 26px; width: 36px; height: 36px;
border-radius: var(--r-xs); border-radius: 10px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.down-icon { background: var(--c-green-dim); color: var(--c-green); } .down-icon { background: var(--c-green-dim); color: var(--c-green); }
.up-icon { background: var(--c-accent-dim); color: var(--c-accent); } .up-icon { background: var(--c-accent-dim); color: var(--c-accent); }
.ping-icon { background: rgba(245,158,11,0.10); color: var(--c-amber); }
.metric-body { .metric-body {
display: flex; display: flex;
@ -449,16 +502,16 @@ input, textarea { font-family: inherit; }
} }
.metric-label { .metric-label {
font-size: 0.58rem; font-size: 0.75rem;
color: var(--c-txt-3); color: rgba(255,255,255,0.54);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.6px; letter-spacing: 0.8px;
font-weight: 600; font-weight: 700;
} }
.metric-value { .metric-value {
font-size: 0.76rem; font-size: 1rem;
font-weight: 600; font-weight: 700;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--c-txt-1); color: var(--c-txt-1);
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
@ -694,29 +747,7 @@ input, textarea { font-family: inherit; }
text-transform: none; text-transform: none;
} }
/* ── Save button ──────────────────────────────────────────────────────────── */
.save-btn {
width: 100%;
height: 44px;
border-radius: var(--r-md);
border: 1px solid var(--c-accent);
background: var(--c-accent-dim);
color: var(--c-accent);
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.3px;
transition: all var(--t-fast);
flex-shrink: 0;
backdrop-filter: blur(8px);
}
.save-btn:hover {
background: var(--c-accent);
color: #fff;
box-shadow: 0 4px 20px var(--c-accent-glow);
}
.save-btn:active { transform: scale(0.98); }
/* ── Toast ────────────────────────────────────────────────────────────────── */ /* ── Toast ────────────────────────────────────────────────────────────────── */
.toast { .toast {

View File

@ -18,3 +18,5 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
lazy_static = "1.4" lazy_static = "1.4"
portable-atomic = { workspace = true } portable-atomic = { workspace = true }
tracing-subscriber = "0.3.23"
tracing.workspace = true

View File

@ -32,7 +32,7 @@ class OstpClientSdk private constructor(private val context: Context) {
// ── Native JNI bindings ─────────────────────────────────────────────────── // ── Native JNI bindings ───────────────────────────────────────────────────
private external fun nativeStartClient(configJson: String, fd: Int): Boolean private external fun nativeStartClient(configJson: String): Boolean
private external fun nativeStopClient(): Boolean private external fun nativeStopClient(): Boolean
private external fun nativeGetMetrics(): String private external fun nativeGetMetrics(): String
private external fun nativeGetLogs(): String private external fun nativeGetLogs(): String
@ -165,7 +165,7 @@ class OstpClientSdk private constructor(private val context: Context) {
* *
* @return `true` if the native layer accepted the start command. * @return `true` if the native layer accepted the start command.
*/ */
fun start(config: Config, fd: Int = -1): Boolean { fun start(config: Config): Boolean {
if (started.getAndSet(true)) { if (started.getAndSet(true)) {
emitLog("SDK already started; call stop() first to change config") emitLog("SDK already started; call stop() first to change config")
return false return false
@ -175,7 +175,7 @@ class OstpClientSdk private constructor(private val context: Context) {
_state.value = TunnelState.Connecting _state.value = TunnelState.Connecting
val json = config.toNativeJson() val json = config.toNativeJson()
val ok = nativeStartClient(json, fd) val ok = nativeStartClient(json)
if (!ok) { if (!ok) {
_state.value = TunnelState.Failed("Native layer rejected config") _state.value = TunnelState.Failed("Native layer rejected config")
started.set(false) started.set(false)

View File

@ -8,12 +8,49 @@ use tokio::runtime::Runtime;
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
use ostp_client::bridge::{Bridge, BridgeMetrics}; use ostp_client::bridge::{Bridge, BridgeMetrics};
use ostp_client::config::ClientConfig; use ostp_client::config::ClientConfig;
use ostp_client::tunnel;
use ostp_client::app::{BridgeCommand, UiEvent}; use ostp_client::app::{BridgeCommand, UiEvent};
use std::io::Write;
struct JniLogWriter;
impl Write for JniLogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let s = String::from_utf8_lossy(buf).trim().to_string();
if !s.is_empty() {
add_log(s);
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for JniLogWriter {
type Writer = JniLogWriter;
fn make_writer(&'a self) -> Self::Writer {
JniLogWriter
}
}
static TRACING_INIT: std::sync::Once = std::sync::Once::new();
fn init_tracing() {
TRACING_INIT.call_once(|| {
let subscriber = tracing_subscriber::fmt()
.with_writer(JniLogWriter)
.with_ansi(false)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
});
}
struct SdkState { struct SdkState {
runtime: Option<Runtime>, runtime: Option<Runtime>,
shutdown_tx: Option<watch::Sender<bool>>, shutdown_tx: Option<watch::Sender<bool>>,
metrics: Option<Arc<BridgeMetrics>>, metrics: Option<Arc<BridgeMetrics>>,
tun_child: Option<std::process::Child>,
cmd_tx: Option<mpsc::Sender<BridgeCommand>>, cmd_tx: Option<mpsc::Sender<BridgeCommand>>,
} }
@ -22,6 +59,7 @@ lazy_static! {
runtime: None, runtime: None,
shutdown_tx: None, shutdown_tx: None,
metrics: None, metrics: None,
tun_child: None,
cmd_tx: None, cmd_tx: None,
}); });
static ref LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new()); static ref LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
@ -39,11 +77,13 @@ fn add_log(text: String) {
} }
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient( pub extern "system" fn Java_net_ostp_client_OstpClientSdk_startClient(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
config_json: JString, config_json: JString,
fd: jni::sys::jint, fd: jni::sys::jint,
t2s_bin_path: JString,
local_proxy: JString,
) -> jboolean { ) -> jboolean {
let mut state = match STATE.lock() { let mut state = match STATE.lock() {
Ok(s) => s, Ok(s) => s,
@ -55,6 +95,8 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
return jni::sys::JNI_TRUE; return jni::sys::JNI_TRUE;
} }
init_tracing();
if let Ok(jvm) = env.get_java_vm() { if let Ok(jvm) = env.get_java_vm() {
if let Ok(mut guard) = JVM.lock() { if let Ok(mut guard) = JVM.lock() {
*guard = Some(jvm); *guard = Some(jvm);
@ -100,6 +142,16 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
Err(_) => return jni::sys::JNI_FALSE, Err(_) => return jni::sys::JNI_FALSE,
}; };
let t2s_path: String = match env.get_string(&t2s_bin_path) {
Ok(s) => s.into(),
Err(_) => return jni::sys::JNI_FALSE,
};
let proxy_addr: String = match env.get_string(&local_proxy) {
Ok(s) => s.into(),
Err(_) => return jni::sys::JNI_FALSE,
};
// Parse config from JSON // Parse config from JSON
let config: ClientConfig = match serde_json::from_str(&config_str) { let config: ClientConfig = match serde_json::from_str(&config_str) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
@ -109,6 +161,8 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
} }
}; };
let debug = config.debug;
// Create tokio runtime // Create tokio runtime
let rt = match Runtime::new() { let rt = match Runtime::new() {
Ok(r) => r, Ok(r) => r,
@ -139,6 +193,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
let (ui_tx, mut ui_rx) = mpsc::channel(512); let (ui_tx, mut ui_rx) = mpsc::channel(512);
let (cmd_tx, cmd_rx) = mpsc::channel(128); let (cmd_tx, cmd_rx) = mpsc::channel(128);
let (shutdown_tx, shutdown_rx) = watch::channel(false); let (shutdown_tx, shutdown_rx) = watch::channel(false);
let proxy_shutdown_rx = shutdown_tx.subscribe();
let metrics_clone = Arc::clone(&metrics); let metrics_clone = Arc::clone(&metrics);
@ -147,40 +202,9 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await
}); });
if config.mode == "tun" {
if fd < 0 {
add_log("Error: TUN mode requested but invalid file descriptor provided".to_string());
return jni::sys::JNI_FALSE;
}
let tun_dev = match ostp_client::tunnel::create_tun_device_from_fd(fd, config.ostp.mtu) {
Ok(d) => d,
Err(e) => {
add_log(format!("Failed to wrap TUN fd: {:?}", e));
return jni::sys::JNI_FALSE;
}
};
let stack_shutdown_rx = shutdown_tx.subscribe();
let proxy_events_tx_clone = proxy_events_tx.clone();
let mtu = config.ostp.mtu;
rt.spawn(async move {
if let Err(e) = ostp_client::tunnel::run_smoltcp_stack(
tun_dev.packet_rx,
tun_dev.packet_tx,
mtu,
proxy_events_tx_clone,
client_msgs_rx,
stack_shutdown_rx,
).await {
add_log(format!("smoltcp stack loop failed: {:?}", e));
}
});
} else {
let config_proxy = config.clone(); let config_proxy = config.clone();
let proxy_shutdown_rx = shutdown_tx.subscribe();
rt.spawn(async move { rt.spawn(async move {
let _ = ostp_client::tunnel::run_local_proxy( tunnel::run_local_proxy(
config_proxy.local_proxy, config_proxy.local_proxy,
config_proxy.ostp, config_proxy.ostp,
config_proxy.exclusions, config_proxy.exclusions,
@ -189,9 +213,8 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
proxy_events_tx, proxy_events_tx,
client_msgs_rx, client_msgs_rx,
) )
.await; .await
}); });
}
// Start logs receiver task // Start logs receiver task
rt.spawn(async move { rt.spawn(async move {
@ -211,6 +234,90 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
let _ = cmd_tx_clone.send(BridgeCommand::ToggleTunnel).await; let _ = cmd_tx_clone.send(BridgeCommand::ToggleTunnel).await;
}); });
if config.tun_stack == "system" {
// Spawn tun2socks
let fd_str = format!("fd://{}", fd);
let proxy_str = format!("socks5://{}", proxy_addr);
if debug {
add_log(format!("Spawning tun2socks: {} -device {} -proxy {}", t2s_path, fd_str, proxy_str));
}
let mut cmd = std::process::Command::new(&t2s_path);
cmd.arg("-device")
.arg(&fd_str)
.arg("-proxy")
.arg(&proxy_str);
if config.ostp.mtu > 0 {
cmd.arg("-mtu").arg(config.ostp.mtu.to_string());
}
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
add_log(format!("Failed to spawn tun2socks from Rust: {e}"));
return jni::sys::JNI_FALSE;
}
};
let stdout = match child.stdout.take() {
Some(s) => s,
None => {
add_log("Failed to capture tun2socks stdout".to_string());
return jni::sys::JNI_FALSE;
}
};
let stderr = match child.stderr.take() {
Some(s) => s,
None => {
add_log("Failed to capture tun2socks stderr".to_string());
return jni::sys::JNI_FALSE;
}
};
// Read stdout
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(l) = line {
if debug {
add_log(format!("tun2socks: {}", l));
}
}
}
});
// Read stderr & wait
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr);
for line in reader.lines() {
if let Ok(l) = line {
if debug {
add_log(format!("tun2socks ERROR: {}", l));
}
}
}
});
state.tun_child = Some(child);
} else {
if debug {
add_log("Using OSTP native TUN stack. Bypassing tun2socks.".to_string());
}
let shutdown_rx_clone = shutdown_tx.subscribe();
let config_clone = config.clone();
rt.spawn(async move {
if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, fd).await {
add_log(format!("Native TUN exited with error: {}", e));
}
});
}
state.runtime = Some(rt); state.runtime = Some(rt);
state.shutdown_tx = Some(shutdown_tx); state.shutdown_tx = Some(shutdown_tx);
state.metrics = Some(metrics_clone); state.metrics = Some(metrics_clone);
@ -221,31 +328,42 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient(
} }
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStopClient( pub extern "system" fn Java_net_ostp_client_OstpClientSdk_stopClient(
_env: JNIEnv, _env: JNIEnv,
_class: JClass, _class: JClass,
) -> jboolean { ) -> jboolean {
let (tun_child, shutdown_tx, runtime) = {
let mut state = match STATE.lock() { let mut state = match STATE.lock() {
Ok(s) => s, Ok(s) => s,
Err(_) => return jni::sys::JNI_FALSE, Err(_) => return jni::sys::JNI_FALSE,
}; };
let c = state.tun_child.take();
let s = state.shutdown_tx.take();
let r = state.runtime.take();
state.cmd_tx = None;
state.metrics = None;
(c, s, r)
};
if let Some(shutdown_tx) = state.shutdown_tx.take() { if let Some(mut child) = tun_child {
let _ = shutdown_tx.send(true); let _ = child.kill();
add_log("Killed tun2socks process".to_string());
} }
if let Some(rt) = state.runtime.take() { if let Some(s) = shutdown_tx {
let _ = s.send(true);
}
if let Some(rt) = runtime {
rt.shutdown_timeout(std::time::Duration::from_secs(3)); rt.shutdown_timeout(std::time::Duration::from_secs(3));
} }
state.cmd_tx = None;
state.metrics = None;
add_log("OSTP SDK: Client successfully stopped".to_string()); add_log("OSTP SDK: Client successfully stopped".to_string());
jni::sys::JNI_TRUE jni::sys::JNI_TRUE
} }
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeGetMetrics( pub extern "system" fn Java_net_ostp_client_OstpClientSdk_getMetrics(
env: JNIEnv, env: JNIEnv,
_class: JClass, _class: JClass,
) -> jstring { ) -> jstring {
@ -266,7 +384,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeGetMetrics(
r#"{{"bytes_sent": {}, "bytes_recv": {}, "connection_state": {}, "rtt_ms": {}}}"#, r#"{{"bytes_sent": {}, "bytes_recv": {}, "connection_state": {}, "rtt_ms": {}}}"#,
sent, recv, conn_state, rtt sent, recv, conn_state, rtt
); );
match env.new_string(json) { match env.new_string(json.replace('\0', "")) {
Ok(s) => s.into_raw(), Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(), Err(_) => std::ptr::null_mut(),
} }
@ -279,7 +397,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeGetMetrics(
} }
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeGetLogs( pub extern "system" fn Java_net_ostp_client_OstpClientSdk_getLogs(
env: JNIEnv, env: JNIEnv,
_class: JClass, _class: JClass,
) -> jstring { ) -> jstring {
@ -293,7 +411,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeGetLogs(
Err(_) => "[]".to_string(), Err(_) => "[]".to_string(),
}; };
match env.new_string(json) { match env.new_string(json.replace('\0', "")) {
Ok(s) => s.into_raw(), Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(), Err(_) => std::ptr::null_mut(),
} }

View File

@ -20,9 +20,10 @@ use portable_atomic::AtomicU64;
use std::time::Instant; use std::time::Instant;
use axum::{ use axum::{
body::Body,
extract::{Path, State}, extract::{Path, State},
http::{header, StatusCode, Uri}, http::{header, Request, StatusCode, Uri},
response::IntoResponse, response::{IntoResponse, Response},
routing::{get, post, put}, routing::{get, post, put},
Json, Router, Json, Router,
}; };

View File

@ -65,6 +65,7 @@ pub(crate) struct RemoteState {
pub async fn run_server( pub async fn run_server(
bind_addrs: Vec<String>, bind_addrs: Vec<String>,
server_public_ip: Option<String>,
access_keys: Vec<(String, crate::api::UserMeta)>, access_keys: Vec<(String, crate::api::UserMeta)>,
outbound: Option<OutboundConfig>, outbound: Option<OutboundConfig>,
api_config: Option<ApiConfig>, api_config: Option<ApiConfig>,
@ -248,7 +249,7 @@ pub async fn run_server(
let primary = bind_addrs.first().cloned().unwrap_or_else(|| "0.0.0.0:50000".to_string()); let primary = bind_addrs.first().cloned().unwrap_or_else(|| "0.0.0.0:50000".to_string());
let parts: Vec<&str> = primary.rsplitn(2, ':').collect(); let parts: Vec<&str> = primary.rsplitn(2, ':').collect();
let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000); let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000);
let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string(); let server_host = server_public_ip.unwrap_or_else(|| parts.get(1).unwrap_or(&"0.0.0.0").to_string());
let rq = reality_query.clone().unwrap_or_default(); let rq = reality_query.clone().unwrap_or_default();
let config_path_api = config_path.clone(); let config_path_api = config_path.clone();
tokio::spawn(async move { tokio::spawn(async move {

View File

@ -894,8 +894,9 @@ async fn run_app() -> Result<()> {
limit_bytes: uc.limit(), limit_bytes: uc.limit(),
}) })
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
let host = get_or_ask_public_ip(&args.config);
// Pass all listen addresses for multi-listener support // Pass all listen addresses for multi-listener support
ostp_server::run_server(listen_addrs, access_keys_meta, outbound, api_config, fallback_config, debug, rq, rc, Some(args.config)).await?; ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, rq, rc, Some(args.config)).await?;
} }
AppMode::Client(client_cfg) => { AppMode::Client(client_cfg) => {
run_client_directly(client_cfg).await?; run_client_directly(client_cfg).await?;
@ -1008,6 +1009,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
let reality_cfg = client_cfg.reality.as_ref(); let reality_cfg = client_cfg.reality.as_ref();
let client_conf = ostp_client::config::ClientConfig { let client_conf = ostp_client::config::ClientConfig {
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
tun_stack: "native".to_string(),
debug: client_cfg.debug.unwrap_or(false), debug: client_cfg.debug.unwrap_or(false),
ostp: ostp_client::config::OstpConfig { ostp: ostp_client::config::OstpConfig {
server_addr: client_cfg.server.clone(), server_addr: client_cfg.server.clone(),