diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 620a7cac..1b58ea54 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -197,7 +197,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -418,11 +418,12 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" -version = "1.3.3" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ "serde", + "unty", ] [[package]] @@ -569,7 +570,7 @@ dependencies = [ "hashbrown 0.14.5", "instant", "once_cell", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -600,7 +601,7 @@ dependencies = [ "cairo-sys-rs", "glib", "libc", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -767,7 +768,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -852,9 +853,12 @@ dependencies = [ [[package]] name = "const_panic" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] [[package]] name = "convert_case" @@ -1036,17 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.71", -] - -[[package]] -name = "cstr" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" -dependencies = [ - "proc-macro2", - "quote", + "syn 2.0.117", ] [[package]] @@ -1056,7 +1050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1104,7 +1098,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1126,7 +1120,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1147,7 +1141,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1160,7 +1154,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1244,27 +1238,37 @@ dependencies = [ [[package]] name = "dll-syringe" -version = "0.15.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdc807201d54de75e9bd7ad199d0031048625059f84acfc94506bdb13c0b4f59" +checksum = "a7048876a2194fb2f949fc7e99bf0adc7c32acebb83e99c719658a0f08b28b6c" dependencies = [ "bincode", - "cstr", + "dll-syringe-macros", "goblin", "iced-x86", "konst", - "num_enum 0.6.1", + "num_enum 0.7.5", "path-absolutize", "same-file", "serde", "shrinkwraprs", "stopwatch2", - "sysinfo 0.29.11", - "thiserror", + "sysinfo 0.37.2", + "thiserror 2.0.18", "widestring", "winapi", ] +[[package]] +name = "dll-syringe-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52aadbd0973e4db2d0781f869c38794ffa03dde3b46eb2fc302a7e6feb5103" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -1538,7 +1542,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1628,7 +1632,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -1831,7 +1835,7 @@ dependencies = [ "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -1864,7 +1868,7 @@ dependencies = [ "libc", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -1924,9 +1928,9 @@ dependencies = [ [[package]] name = "goblin" -version = "0.6.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" dependencies = [ "log", "plain", @@ -2271,7 +2275,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2489,7 +2493,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.62", "walkdir", ] @@ -2531,7 +2535,7 @@ checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" dependencies = [ "serde", "serde_json", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -2543,7 +2547,7 @@ dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -2559,21 +2563,11 @@ dependencies = [ [[package]] name = "konst" -version = "0.3.9" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" dependencies = [ "const_panic", - "konst_kernel", - "typewit", -] - -[[package]] -name = "konst_kernel" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" -dependencies = [ "typewit", ] @@ -2604,9 +2598,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libloading" @@ -2817,7 +2811,7 @@ dependencies = [ "jni-sys", "ndk-sys", "num_enum 0.5.11", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -2934,11 +2928,12 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.6.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "num_enum_derive 0.6.1", + "num_enum_derive 0.7.5", + "rustversion", ] [[package]] @@ -2955,13 +2950,13 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.6.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -3029,6 +3024,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "objc2-core-image" version = "0.2.2" @@ -3059,6 +3063,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-metal" version = "0.2.2" @@ -3190,7 +3204,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -3451,7 +3465,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -3498,7 +3512,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -3611,9 +3625,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3795,7 +3809,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.62", ] [[package]] @@ -4056,22 +4070,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scroll" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" dependencies = [ "scroll_derive", ] [[package]] name = "scroll_derive" -version = "0.11.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -4143,7 +4157,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -4166,7 +4180,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -4217,7 +4231,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -4239,7 +4253,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -4488,9 +4502,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4514,31 +4528,31 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.11" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", - "winapi", + "rayon", + "windows 0.52.0", ] [[package]] name = "sysinfo" -version = "0.30.13" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ - "cfg-if", - "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", - "rayon", - "windows 0.52.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", ] [[package]] @@ -4631,7 +4645,7 @@ dependencies = [ "unicode-segmentation", "uuid", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", "x11-dl", ] @@ -4708,7 +4722,7 @@ dependencies = [ "tauri-runtime-wry", "tauri-utils", "tempfile", - "thiserror", + "thiserror 1.0.62", "tokio", "url", "uuid", @@ -4756,7 +4770,7 @@ dependencies = [ "serde_json", "sha2", "tauri-utils", - "thiserror", + "thiserror 1.0.62", "time", "uuid", "walkdir", @@ -4788,7 +4802,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "thiserror", + "thiserror 1.0.62", "tokio", "tokio-util", ] @@ -4807,7 +4821,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror", + "thiserror 1.0.62", "url", "uuid", "webview2-com", @@ -4859,7 +4873,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror", + "thiserror 1.0.62", "url", "walkdir", "windows-version", @@ -4919,7 +4933,16 @@ version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.62", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -4930,7 +4953,18 @@ checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -5044,7 +5078,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -5196,7 +5230,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] @@ -5265,18 +5299,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typewit" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" +version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" [[package]] name = "unic" @@ -5613,6 +5638,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.2" @@ -5748,7 +5779,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -5782,7 +5813,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5945,7 +5976,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", ] [[package]] @@ -5968,7 +5999,7 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror", + "thiserror 1.0.62", "windows 0.39.0", "windows-bindgen", "windows-metadata", @@ -5994,9 +6025,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -6048,7 +6079,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" dependencies = [ - "windows-implement", + "windows-implement 0.39.0", "windows_aarch64_msvc 0.39.0", "windows_i686_gnu 0.39.0", "windows_i686_msvc 0.39.0", @@ -6071,10 +6102,23 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + [[package]] name = "windows-bindgen" version = "0.39.0" @@ -6085,6 +6129,15 @@ dependencies = [ "windows-tokens", ] +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -6094,6 +6147,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.39.0" @@ -6104,12 +6181,68 @@ dependencies = [ "windows-tokens", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-metadata" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -6183,6 +6316,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-tokens" version = "0.39.0" @@ -6430,7 +6572,7 @@ dependencies = [ "nix", "os_pipe", "tempfile", - "thiserror", + "thiserror 1.0.62", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -6467,13 +6609,13 @@ dependencies = [ "sha2", "soup2", "tao", - "thiserror", + "thiserror 1.0.62", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", "windows 0.39.0", - "windows-implement", + "windows-implement 0.39.0", ] [[package]] @@ -6542,7 +6684,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.117", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f71ea4a..f2648545 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -51,7 +51,7 @@ windows-sys = { version = "0.52.0", features = [ "Win32_System_JobObjects" ] } winreg = "0.52.0" -dll-syringe = "0.15.2" +dll-syringe = "0.17.1" windows = { version = "0.39.0", features = [ "Win32_System_Console", "Win32_Foundation", diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 795b9b7c..d860e1e6 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/src/bin/omp_connect_probe.rs b/src-tauri/src/bin/omp_connect_probe.rs new file mode 100644 index 00000000..7343ac0c --- /dev/null +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -0,0 +1,163 @@ +use std::env; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::time::Duration; + +const ID_OPEN_CONNECTION_REQUEST: u8 = 24; +const ID_OPEN_CONNECTION_REPLY: u8 = 25; +const ID_OPEN_CONNECTION_COOKIE: u8 = 26; +const ID_CONNECTION_ATTEMPT_FAILED: u8 = 29; +const ID_NO_FREE_INCOMING_CONNECTIONS: u8 = 31; +const SAMP_PETARDED: u16 = 0x6969; + +fn print_usage() { + eprintln!("Usage: omp_connect_probe "); + eprintln!(" family: ipv4 | ipv6"); +} + +fn parse_family(input: &str) -> Option { + match input { + "ipv4" => Some(false), + "ipv6" => Some(true), + _ => None, + } +} + +fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result { + if let Ok(ip) = host + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + { + return Ok(SocketAddr::new(ip, port)); + } + + let mut addrs = (host, port) + .to_socket_addrs() + .map_err(|e| format!("failed to resolve {host}:{port}: {e}"))?; + + addrs + .find(|addr| addr.is_ipv6() == want_ipv6) + .ok_or_else(|| { + format!( + "no {} address found for {}", + if want_ipv6 { "IPv6" } else { "IPv4" }, + host + ) + }) +} + +fn main() { + let args: Vec = env::args().collect(); + if args.len() != 4 { + print_usage(); + std::process::exit(1); + } + + let want_ipv6 = match parse_family(&args[1]) { + Some(v) => v, + None => { + eprintln!("invalid family: {}", args[1]); + std::process::exit(1); + } + }; + + let host = &args[2]; + let port = match args[3].parse::() { + Ok(v) => v, + Err(e) => { + eprintln!("invalid port {}: {}", args[3], e); + std::process::exit(1); + } + }; + + let target = match resolve_target(host, port, want_ipv6) { + Ok(target) => target, + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + }; + + let bind_addr = if want_ipv6 { "[::]:0" } else { "0.0.0.0:0" }; + let socket = match UdpSocket::bind(bind_addr) { + Ok(socket) => socket, + Err(e) => { + eprintln!("bind failed on {bind_addr}: {e}"); + std::process::exit(1); + } + }; + socket.set_read_timeout(Some(Duration::from_secs(2))).ok(); + + let mut response = [0_u8; 128]; + let send_request = |cookie_xor: u16| { + let request = [ + ID_OPEN_CONNECTION_REQUEST, + (cookie_xor & 0xFF) as u8, + ((cookie_xor >> 8) & 0xFF) as u8, + ]; + socket.send_to(&request, target) + }; + + if let Err(e) = send_request(0) { + eprintln!("send_to failed: {e}"); + std::process::exit(1); + } + + let mut received = match socket.recv(&mut response) { + Ok(n) => n, + Err(e) => { + eprintln!("recv failed: {e}"); + std::process::exit(1); + } + }; + + let mut packet_id = response[0]; + if packet_id == ID_OPEN_CONNECTION_COOKIE && received >= 3 { + let cookie = u16::from_le_bytes([response[1], response[2]]); + let cookie_xor = cookie ^ SAMP_PETARDED; + println!("received {} bytes", received); + println!("packet_id={}", packet_id); + println!("result=open_connection_cookie"); + + if let Err(e) = send_request(cookie_xor) { + eprintln!("send_to failed: {e}"); + std::process::exit(1); + } + + received = match socket.recv(&mut response) { + Ok(n) => n, + Err(e) => { + eprintln!("recv failed after cookie exchange: {e}"); + std::process::exit(1); + } + }; + packet_id = response[0]; + } + + println!("received {} bytes", received); + println!("packet_id={}", packet_id); + + match packet_id { + ID_OPEN_CONNECTION_REPLY => { + println!("result=open_connection_reply"); + } + ID_CONNECTION_ATTEMPT_FAILED => { + println!("result=connection_attempt_failed"); + } + ID_NO_FREE_INCOMING_CONNECTIONS => { + println!("result=no_free_incoming_connections"); + } + _ => { + println!("result=unexpected"); + std::process::exit(2); + } + } + + for (index, byte) in response[..received].iter().enumerate() { + if index > 0 { + print!(" "); + } + print!("{byte:02x}"); + } + println!(); +} diff --git a/src-tauri/src/bin/omp_query_probe.rs b/src-tauri/src/bin/omp_query_probe.rs new file mode 100644 index 00000000..3862b39b --- /dev/null +++ b/src-tauri/src/bin/omp_query_probe.rs @@ -0,0 +1,147 @@ +use std::env; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::time::Duration; + +const SAMP_HEADER: &[u8] = b"SAMP"; +const SAMP6_HEADER: &[u8] = b"SAMP6"; + +fn print_usage() { + eprintln!("Usage: omp_query_probe [opcode]"); + eprintln!(" family: ipv4 | ipv6"); + eprintln!(" opcode: i | o | c | r | p"); +} + +fn parse_family(input: &str) -> Option { + match input { + "ipv4" => Some(false), + "ipv6" => Some(true), + _ => None, + } +} + +fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result { + if let Ok(ip) = host + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + { + return Ok(SocketAddr::new(ip, port)); + } + + let mut addrs = (host, port) + .to_socket_addrs() + .map_err(|e| format!("failed to resolve {host}:{port}: {e}"))?; + + addrs + .find(|addr| addr.is_ipv6() == want_ipv6) + .ok_or_else(|| { + format!( + "no {} address found for {}", + if want_ipv6 { "IPv6" } else { "IPv4" }, + host + ) + }) +} + +fn build_packet(target: SocketAddr, opcode: u8) -> Vec { + let mut packet = Vec::new(); + match target.ip() { + IpAddr::V4(ip) => { + packet.extend_from_slice(SAMP_HEADER); + packet.extend_from_slice(&ip.octets()); + } + IpAddr::V6(ip) => { + packet.extend_from_slice(SAMP6_HEADER); + packet.extend_from_slice(&ip.octets()); + } + } + + packet.push((target.port() & 0xFF) as u8); + packet.push(((target.port() >> 8) & 0xFF) as u8); + packet.push(opcode); + + if opcode == b'p' { + packet.extend_from_slice(&[0, 0, 0, 0]); + } + + packet +} + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 4 || args.len() > 5 { + print_usage(); + std::process::exit(1); + } + + let want_ipv6 = match parse_family(&args[1]) { + Some(v) => v, + None => { + eprintln!("invalid family: {}", args[1]); + std::process::exit(1); + } + }; + + let host = &args[2]; + let port = match args[3].parse::() { + Ok(v) => v, + Err(e) => { + eprintln!("invalid port {}: {}", args[3], e); + std::process::exit(1); + } + }; + let opcode = args + .get(4) + .and_then(|value| value.as_bytes().first().copied()) + .unwrap_or(b'i'); + + let target = match resolve_target(host, port, want_ipv6) { + Ok(target) => target, + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + }; + + let bind_addr = if want_ipv6 { "[::]:0" } else { "0.0.0.0:0" }; + let socket = match UdpSocket::bind(bind_addr) { + Ok(socket) => socket, + Err(e) => { + eprintln!("bind failed on {bind_addr}: {e}"); + std::process::exit(1); + } + }; + socket.set_read_timeout(Some(Duration::from_secs(2))).ok(); + + let packet = build_packet(target, opcode); + if let Err(e) = socket.send_to(&packet, target) { + eprintln!("send_to failed: {e}"); + std::process::exit(1); + } + + let mut response = [0_u8; 2048]; + let received = match socket.recv(&mut response) { + Ok(n) => n, + Err(e) => { + eprintln!("recv failed: {e}"); + std::process::exit(1); + } + }; + + println!("received {} bytes for opcode {}", received, opcode as char); + if received >= 24 && &response[..5] == SAMP6_HEADER { + println!("magic=SAMP6 opcode={}", response[23] as char); + } else if received >= 11 && &response[..4] == SAMP_HEADER { + println!("magic=SAMP opcode={}", response[10] as char); + } else { + println!("magic=unknown"); + } + + for (index, byte) in response[..received].iter().enumerate() { + if index > 0 { + print!(" "); + } + print!("{byte:02x}"); + } + println!(); +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 19369ceb..6aba79a1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,9 +1,13 @@ -use crate::{errors::LauncherError, helpers, injector, samp}; +use crate::{constants::SAMP6_PACKET_HEADER, errors::LauncherError, helpers, injector, samp}; use log::{error, info, warn}; use md5::compute; use sevenz_rust::decompress_file; use std::fs::File; use std::io::Read; +use std::net::{IpAddr, SocketAddr}; +use std::time::Duration; +use tokio::net::{lookup_host, UdpSocket}; +use tokio::time::timeout; #[tauri::command] pub async fn inject( @@ -12,6 +16,10 @@ pub async fn inject( port: i32, exe: &str, dll: &str, + trace_file: &str, + trace_dualstack: Option, + trace_remote_ip: Option, + trace_remote_port: Option, omp_file: &str, password: &str, custom_game_exe: &str, @@ -28,6 +36,10 @@ pub async fn inject( port, exe, dll, + trace_file, + trace_dualstack.unwrap_or(false), + trace_remote_ip.as_deref().unwrap_or(""), + trace_remote_port.unwrap_or(0), actual_omp_file, password, custom_game_exe, @@ -73,31 +85,146 @@ pub fn rerun_as_admin() -> std::result::Result { Ok("SUCCESS".to_string()) } +#[tauri::command] +pub fn get_launcher_directory() -> std::result::Result { + let exe_path = std::env::current_exe() + .map_err(|e| format!("Failed to get current executable path: {}", e))?; + + let launcher_dir = exe_path + .parent() + .ok_or_else(|| "Failed to determine launcher directory".to_string())?; + + launcher_dir + .to_str() + .map(|path| path.to_string()) + .ok_or_else(|| "Launcher directory contains invalid UTF-8".to_string()) +} + #[tauri::command] pub fn get_samp_favorite_list() -> String { samp::get_samp_favorite_list() } #[tauri::command] -pub fn resolve_hostname(hostname: String) -> std::result::Result { - use std::net::{IpAddr, ToSocketAddrs}; +pub fn resolve_hostname( + hostname: String, + family: Option, +) -> std::result::Result { + use std::net::ToSocketAddrs; if hostname.is_empty() { return Err("Hostname cannot be empty".to_string()); } - let addr = format!("{}:80", hostname); + let desired_family = match family.as_deref() { + Some("ipv4") => Some(false), + Some("ipv6") => Some(true), + Some(other) => { + return Err(format!( + "Invalid family '{}', expected 'ipv4' or 'ipv6'", + other + )) + } + None => None, + }; + + let normalized = hostname + .trim() + .trim_start_matches('[') + .trim_end_matches(']'); + if let Ok(ip) = normalized.parse::() { + if let Some(want_ipv6) = desired_family { + if ip.is_ipv6() != want_ipv6 { + return Err(format!( + "Hostname '{}' does not match requested family '{}'", + hostname, + if want_ipv6 { "ipv6" } else { "ipv4" } + )); + } + } + return Ok(ip.to_string()); + } + + let addr = format!("{}:80", normalized); let addrs = addr .to_socket_addrs() - .map_err(|e| format!("Failed to resolve hostname '{}': {}", hostname, e))?; + .map_err(|e| format!("Failed to resolve hostname '{}': {}", normalized, e))?; + + for socket_addr in addrs { + if desired_family.map_or(true, |want_ipv6| socket_addr.is_ipv6() == want_ipv6) { + return Ok(socket_addr.ip().to_string()); + } + } + + match desired_family { + Some(true) => Err(format!( + "No IPv6 address found for hostname '{}'", + normalized + )), + Some(false) => Err(format!( + "No IPv4 address found for hostname '{}'", + normalized + )), + None => Err(format!("No address found for hostname '{}'", normalized)), + } +} - for ip in addrs { - if let IpAddr::V4(ipv4) = ip.ip() { - return Ok(ipv4.to_string()); +#[tauri::command] +pub async fn probe_ipv6_query(host: String, port: i32) -> std::result::Result { + if port < 1 || port > 65535 { + return Err(format!("Invalid port '{}'", port)); + } + + let normalized = host.trim().trim_start_matches('[').trim_end_matches(']'); + let target = if let Ok(ip) = normalized.parse::() { + if !ip.is_ipv6() { + return Ok(false); + } + SocketAddr::new(ip, port as u16) + } else { + let mut addrs = lookup_host(format!("{}:{}", normalized, port)) + .await + .map_err(|e| format!("Failed to resolve hostname '{}': {}", normalized, e))?; + + if let Some(addr) = addrs.find(|addr| addr.is_ipv6()) { + addr + } else { + return Ok(false); } + }; + + let socket = match UdpSocket::bind("[::]:0").await { + Ok(socket) => socket, + Err(_) => return Ok(false), + }; + + if socket.connect(target).await.is_err() { + return Ok(false); } - Err(format!("No IPv4 address found for hostname '{}'", hostname)) + let mut packet: Vec = Vec::with_capacity(24); + if let IpAddr::V6(address) = target.ip() { + packet.extend_from_slice(SAMP6_PACKET_HEADER); + packet.extend_from_slice(&address.octets()); + } else { + return Ok(false); + } + + packet.push((target.port() & 0xFF) as u8); + packet.push((target.port() >> 8 & 0xFF) as u8); + packet.push(b'i'); + + if socket.send(&packet).await.is_err() { + return Ok(false); + } + + let mut buf = [0u8; 2048]; + let received = match timeout(Duration::from_millis(1500), socket.recv(&mut buf)).await { + Ok(Ok(n)) => n, + _ => return Ok(false), + }; + + Ok(received >= 24 && &buf[..5] == SAMP6_PACKET_HEADER) } #[tauri::command] diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 088479f8..886768e9 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -19,11 +19,13 @@ pub const OMP_EXTRA_INFO_UPDATE_COOLDOWN_SECS: u64 = 3; pub const INJECTION_MAX_RETRIES: u32 = 5; pub const INJECTION_RETRY_DELAY_MS: u64 = 500; +pub const MODULE_WAIT_MAX_RETRIES: u32 = 40; pub const UDP_BUFFER_SIZE: usize = 1500; pub const PROCESS_MODULE_BUFFER_SIZE: usize = 1024; pub const SAMP_PACKET_HEADER: &[u8] = b"SAMP"; +pub const SAMP6_PACKET_HEADER: &[u8] = b"SAMP6"; pub const QUERY_TYPE_INFO: char = 'i'; pub const QUERY_TYPE_PLAYERS: char = 'c'; diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 98c59287..3e73bae3 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -10,6 +10,24 @@ use std::process::{Command, Stdio}; #[cfg(target_os = "windows")] use crate::{constants::*, errors::*}; +#[cfg(target_os = "windows")] +fn inject_optional_dll(child: u32, dll_path: &str) -> Result<()> { + inject_dll(child, dll_path, INJECTION_MAX_RETRIES, true) +} + +#[cfg(target_os = "windows")] +fn format_injection_error_message(error: &impl std::fmt::Display) -> String { + let raw = error.to_string(); + if raw.contains("os error 126") || raw.contains("The specified module could not be found") { + format!( + "{} (the target process could not load the DLL or one of its dependencies)", + raw + ) + } else { + raw + } +} + #[cfg(not(target_os = "windows"))] pub async fn run_samp( _name: &str, @@ -17,6 +35,10 @@ pub async fn run_samp( _port: i32, _executable_dir: &str, _dll_path: &str, + _trace_file: &str, + _trace_dualstack: bool, + _trace_remote_ip: &str, + _trace_remote_port: i32, _omp_file: &str, _password: &str, _custom_game_exe: &str, @@ -31,6 +53,10 @@ pub async fn run_samp( port: i32, executable_dir: &str, dll_path: &str, + trace_file: &str, + trace_dualstack: bool, + trace_remote_ip: &str, + trace_remote_port: i32, omp_file: &str, password: &str, custom_game_exe: &str, @@ -67,13 +93,51 @@ pub async fn run_samp( ready_for_exec = ready_for_exec.arg("-z").arg(password); } + if !trace_file.is_empty() { + ready_for_exec = ready_for_exec.env( + "OMP_TRACE_DUALSTACK", + if trace_dualstack { "1" } else { "0" }, + ); + + if trace_dualstack + && !trace_remote_ip.is_empty() + && (1..=65535).contains(&trace_remote_port) + { + ready_for_exec = ready_for_exec + .env("OMP_TRACE_REMOTE_IPV6", trace_remote_ip) + .env("OMP_TRACE_REMOTE_PORT", trace_remote_port.to_string()); + } + } + + info!( + "[run_samp] launching {} for {}:{} trace={} omp={}", + exe_path.display(), + ip, + port, + !trace_file.is_empty(), + !omp_file.is_empty() + ); + let process = ready_for_exec.current_dir(executable_dir).spawn(); match process { Ok(p) => { + info!("[run_samp] spawned process pid={}", p.id()); + if !trace_file.is_empty() { + info!("[run_samp] injecting optional trace DLL {}", trace_file); + if let Err(e) = inject_optional_dll(p.id(), trace_file) { + let error_text = format_injection_error_message(&e); + info!( + "[run_samp] optional trace DLL injection failed for {}: {}", + trace_file, error_text + ); + } + } + info!("[run_samp] injecting primary DLL {}", dll_path); inject_dll(p.id(), dll_path, 0, false)?; info!("[run_samp] omp_file.is_empty(): {}", omp_file.is_empty()); if !omp_file.is_empty() { + info!("[run_samp] injecting OMP DLL {}", omp_file); inject_dll(p.id(), omp_file, 0, false) } else { Ok(()) @@ -129,9 +193,25 @@ pub fn inject_dll(child: u32, dll_path: &str, times: u32, waiting_for_vorbis: bo let mut bytes = [0i8; PROCESS_MODULE_BUFFER_SIZE]; if found == 0 { + let next_attempt = times + 1; + if next_attempt > MODULE_WAIT_MAX_RETRIES { + return Err(LauncherError::Injection(format!( + "DLL injection timed out waiting for process modules: {}", + dll_path + ))); + } + if next_attempt == 1 + || next_attempt == MODULE_WAIT_MAX_RETRIES + || next_attempt % 5 == 0 + { + info!( + "[injector.rs] waiting for process modules before injecting {} (attempt {}/{})", + dll_path, next_attempt, MODULE_WAIT_MAX_RETRIES + ); + } let delay = std::time::Duration::from_millis(INJECTION_RETRY_DELAY_MS); std::thread::sleep(delay); - return inject_dll(child, dll_path, times, true); + return inject_dll(child, dll_path, next_attempt, true); } let mut found_vorbis = false; @@ -151,9 +231,25 @@ pub fn inject_dll(child: u32, dll_path: &str, times: u32, waiting_for_vorbis: bo } if !found_vorbis { + let next_attempt = times + 1; + if next_attempt > MODULE_WAIT_MAX_RETRIES { + return Err(LauncherError::Injection(format!( + "DLL injection timed out waiting for vorbis: {}", + dll_path + ))); + } + if next_attempt == 1 + || next_attempt == MODULE_WAIT_MAX_RETRIES + || next_attempt % 5 == 0 + { + info!( + "[injector.rs] waiting for vorbis before injecting {} (attempt {}/{})", + dll_path, next_attempt, MODULE_WAIT_MAX_RETRIES + ); + } let delay = std::time::Duration::from_millis(INJECTION_RETRY_DELAY_MS); std::thread::sleep(delay); - return inject_dll(child, dll_path, times, true); + return inject_dll(child, dll_path, next_attempt, true); } } } @@ -162,16 +258,23 @@ pub fn inject_dll(child: u32, dll_path: &str, times: u32, waiting_for_vorbis: bo let syringe = Syringe::for_process(p); // inject the payload into the target process + info!( + "[injector.rs] attempting DLL injection {} (attempt {}, waiting_for_vorbis={})", + dll_path, + times + 1, + waiting_for_vorbis + ); match syringe.inject(dll_path) { Ok(_) => Ok(()), Err(e) => { + let error_text = format_injection_error_message(&e); let delay = std::time::Duration::from_millis(INJECTION_RETRY_DELAY_MS); std::thread::sleep(delay); if times >= INJECTION_MAX_RETRIES { info!( "[injector.rs] DLL {} injection failed after {} attempts: {}", - dll_path, INJECTION_MAX_RETRIES, e + dll_path, INJECTION_MAX_RETRIES, error_text ); if !waiting_for_vorbis { @@ -179,7 +282,7 @@ pub fn inject_dll(child: u32, dll_path: &str, times: u32, waiting_for_vorbis: bo } return Err(LauncherError::Injection(format!( "DLL injection failed: {}", - e + error_text ))); } diff --git a/src-tauri/src/ipv6_proxy.rs b/src-tauri/src/ipv6_proxy.rs new file mode 100644 index 00000000..5f1bb311 --- /dev/null +++ b/src-tauri/src/ipv6_proxy.rs @@ -0,0 +1,538 @@ +use crate::{ + constants::{SAMP6_PACKET_HEADER, SAMP_PACKET_HEADER, UDP_BUFFER_SIZE}, + errors::{LauncherError, Result}, +}; +use log::{info, warn}; +use once_cell::sync::Lazy; +use serde::Serialize; +use std::fmt::Write as _; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use tokio::net::{lookup_host, UdpSocket}; +use tokio::sync::{oneshot, Mutex}; +use tokio::task::JoinHandle; +use tokio::time::{sleep, timeout, Duration}; + +#[derive(Serialize)] +pub struct ProxyInfo { + pub host: String, + pub port: u16, +} + +struct RunningProxy { + stop: oneshot::Sender<()>, + task: JoinHandle<()>, +} + +static RUNNING_PROXY: Lazy>> = Lazy::new(|| Mutex::new(None)); +const LOCAL_BIND_RETRY_COUNT: usize = 10; +const LOCAL_BIND_RETRY_DELAY_MS: u64 = 50; +const PROXY_STOP_WAIT_MS: u64 = 1000; +const PACKET_PREVIEW_BYTES: usize = 12; +const LEGACY_OPEN_CONNECTION_REQUEST_NEW: u8 = 24; +const LEGACY_OPEN_CONNECTION_REQUEST_OLD: u8 = 10; + +const SAMP_LEGACY_DECRYPT_KEY: [u8; 256] = [ + 0xB4, 0x62, 0x07, 0xE5, 0x9D, 0xAF, 0x63, 0xDD, 0xE3, 0xD0, 0xCC, 0xFE, 0xDC, 0xDB, 0x6B, + 0x2E, 0x6A, 0x40, 0xAB, 0x47, 0xC9, 0xD1, 0x53, 0xD5, 0x20, 0x91, 0xA5, 0x0E, 0x4A, 0xDF, + 0x18, 0x89, 0xFD, 0x6F, 0x25, 0x12, 0xB7, 0x13, 0x77, 0x00, 0x65, 0x36, 0x6D, 0x49, 0xEC, + 0x57, 0x2A, 0xA9, 0x11, 0x5F, 0xFA, 0x78, 0x95, 0xA4, 0xBD, 0x1E, 0xD9, 0x79, 0x44, 0xCD, + 0xDE, 0x81, 0xEB, 0x09, 0x3E, 0xF6, 0xEE, 0xDA, 0x7F, 0xA3, 0x1A, 0xA7, 0x2D, 0xA6, 0xAD, + 0xC1, 0x46, 0x93, 0xD2, 0x1B, 0x9C, 0xAA, 0xD7, 0x4E, 0x4B, 0x4D, 0x4C, 0xF3, 0xB8, 0x34, + 0xC0, 0xCA, 0x88, 0xF4, 0x94, 0xCB, 0x04, 0x39, 0x30, 0x82, 0xD6, 0x73, 0xB0, 0xBF, 0x22, + 0x01, 0x41, 0x6E, 0x48, 0x2C, 0xA8, 0x75, 0xB1, 0x0A, 0xAE, 0x9F, 0x27, 0x80, 0x10, 0xCE, + 0xF0, 0x29, 0x28, 0x85, 0x0D, 0x05, 0xF7, 0x35, 0xBB, 0xBC, 0x15, 0x06, 0xF5, 0x60, 0x71, + 0x03, 0x1F, 0xEA, 0x5A, 0x33, 0x92, 0x8D, 0xE7, 0x90, 0x5B, 0xE9, 0xCF, 0x9E, 0xD3, 0x5D, + 0xED, 0x31, 0x1C, 0x0B, 0x52, 0x16, 0x51, 0x0F, 0x86, 0xC5, 0x68, 0x9B, 0x21, 0x0C, 0x8B, + 0x42, 0x87, 0xFF, 0x4F, 0xBE, 0xC8, 0xE8, 0xC7, 0xD4, 0x7A, 0xE0, 0x55, 0x2F, 0x8A, 0x8E, + 0xBA, 0x98, 0x37, 0xE4, 0xB2, 0x38, 0xA1, 0xB6, 0x32, 0x83, 0x3A, 0x7B, 0x84, 0x3C, 0x61, + 0xFB, 0x8C, 0x14, 0x3D, 0x43, 0x3B, 0x1D, 0xC3, 0xA2, 0x96, 0xB3, 0xF8, 0xC4, 0xF2, 0x26, + 0x2B, 0xD8, 0x7C, 0xFC, 0x23, 0x24, 0x66, 0xEF, 0x69, 0x64, 0x50, 0x54, 0x59, 0xF1, 0xA0, + 0x74, 0xAC, 0xC6, 0x7D, 0xB5, 0xE6, 0xE2, 0xC2, 0x7E, 0x67, 0x17, 0x5E, 0xE1, 0xB9, 0x3F, + 0x6C, 0x70, 0x08, 0x99, 0x45, 0x56, 0x76, 0xF9, 0x9A, 0x97, 0x19, 0x72, 0x5C, 0x02, 0x8F, + 0x58, +]; + +static SAMP_LEGACY_ENCRYPT_KEY: Lazy<[u8; 256]> = Lazy::new(|| { + let mut inverse = [0u8; 256]; + for (index, value) in SAMP_LEGACY_DECRYPT_KEY.iter().enumerate() { + inverse[*value as usize] = index as u8; + } + inverse +}); + +async fn stop_running_proxy_task() { + let previous = { + let mut running_proxy = RUNNING_PROXY.lock().await; + running_proxy.take() + }; + + if let Some(existing) = previous { + info!("[ipv6-proxy] stopping previous proxy task"); + let _ = existing.stop.send(()); + + match timeout(Duration::from_millis(PROXY_STOP_WAIT_MS), existing.task).await { + Ok(Ok(())) => {} + Ok(Err(error)) => { + warn!("[ipv6-proxy] previous proxy task join failed: {}", error); + } + Err(_) => { + warn!("[ipv6-proxy] previous proxy task did not stop in time; aborting"); + } + } + } +} + +async fn resolve_ipv6_target(host: &str, port: u16) -> Result { + let normalized = host.trim().trim_start_matches('[').trim_end_matches(']'); + + if let Ok(ip) = normalized.parse::() { + return match ip { + IpAddr::V6(_) => Ok(SocketAddr::new(ip, port)), + IpAddr::V4(_) => Err(LauncherError::InvalidInput(format!( + "Expected an IPv6 target, got IPv4 '{}'", + normalized + ))), + }; + } + + let mut addrs = lookup_host(format!("{}:{}", normalized, port)) + .await + .map_err(|e| { + LauncherError::Network(format!("Failed to resolve '{}': {}", normalized, e)) + })?; + + addrs.find(|addr| addr.is_ipv6()).ok_or_else(|| { + LauncherError::NotFound(format!("No IPv6 address found for '{}'", normalized)) + }) +} + +fn rewrite_client_packet_for_ipv6(packet: &[u8], remote: SocketAddr) -> Vec { + if packet.len() >= 11 && &packet[..4] == SAMP_PACKET_HEADER { + if let IpAddr::V6(address) = remote.ip() { + let mut rewritten = Vec::with_capacity(packet.len() + 13); + rewritten.extend_from_slice(SAMP6_PACKET_HEADER); + rewritten.extend_from_slice(&address.octets()); + rewritten.push((remote.port() & 0xFF) as u8); + rewritten.push(((remote.port() >> 8) & 0xFF) as u8); + rewritten.extend_from_slice(&packet[10..]); + return rewritten; + } + } + + packet.to_vec() +} + +fn rewrite_server_packet_for_ipv4(packet: &[u8], local_port: u16) -> Vec { + if packet.len() >= 24 && &packet[..5] == SAMP6_PACKET_HEADER { + let mut rewritten = Vec::with_capacity(packet.len() - 13); + rewritten.extend_from_slice(SAMP_PACKET_HEADER); + rewritten.extend_from_slice(&Ipv4Addr::LOCALHOST.octets()); + rewritten.push((local_port & 0xFF) as u8); + rewritten.push(((local_port >> 8) & 0xFF) as u8); + rewritten.extend_from_slice(&packet[23..]); + return rewritten; + } + + packet.to_vec() +} + +fn decrypt_legacy_samp_packet(packet: &[u8], port: u16) -> Option> { + if packet.len() < 2 { + return None; + } + + let mut decrypted = Vec::with_capacity(packet.len().saturating_sub(1)); + let mut checksum = 0u8; + let port_mask = (port as u8) ^ 0xCC; + + for (index, byte) in packet.iter().enumerate().skip(1) { + let mut value = *byte; + if index % 2 == 0 { + value ^= port_mask; + } + + let plain = SAMP_LEGACY_DECRYPT_KEY[value as usize]; + checksum ^= plain & 0xAA; + decrypted.push(plain); + } + + if packet[0] != checksum { + return None; + } + + Some(decrypted) +} + +fn encrypt_legacy_samp_packet(payload: &[u8], port: u16) -> Vec { + let mut encrypted = Vec::with_capacity(payload.len() + 1); + let mut checksum = 0u8; + let port_mask = (port as u8) ^ 0xCC; + + encrypted.push(0); + for (index, byte) in payload.iter().enumerate() { + checksum ^= *byte & 0xAA; + + let mut value = SAMP_LEGACY_ENCRYPT_KEY[*byte as usize]; + if (index + 1) % 2 == 0 { + value ^= port_mask; + } + + encrypted.push(value); + } + + encrypted[0] = checksum; + encrypted +} + +fn is_legacy_open_connection_request(packet_id: u8) -> bool { + packet_id == LEGACY_OPEN_CONNECTION_REQUEST_NEW || packet_id == LEGACY_OPEN_CONNECTION_REQUEST_OLD +} + +fn is_likely_legacy_server_control(packet_id: u8) -> bool { + matches!( + packet_id, + 11 | 12 | 17 | 19 + | 25 + | 26 + | 29 + | 30 + | 31 + | 32 + ) +} + +fn rewrite_client_packet( + packet: &[u8], + remote: SocketAddr, + local_port: u16, + legacy_client_cipher_active: &mut bool, +) -> Vec { + if packet.len() >= 11 && &packet[..4] == SAMP_PACKET_HEADER { + return rewrite_client_packet_for_ipv6(packet, remote); + } + + let Some(decrypted) = decrypt_legacy_samp_packet(packet, local_port) else { + return packet.to_vec(); + }; + + let packet_id = decrypted[0]; + if !*legacy_client_cipher_active { + if !is_legacy_open_connection_request(packet_id) { + return packet.to_vec(); + } + + *legacy_client_cipher_active = true; + info!( + "[ipv6-proxy] enabled legacy client cipher translation (id={}, local_port={}, remote_port={})", + packet_id, + local_port, + remote.port() + ); + } + + encrypt_legacy_samp_packet(&decrypted, remote.port()) +} + +fn rewrite_server_packet( + packet: &[u8], + local_port: u16, + remote_port: u16, + legacy_remote_cipher_active: &mut bool, +) -> Vec { + if packet.len() >= 24 && &packet[..5] == SAMP6_PACKET_HEADER { + return rewrite_server_packet_for_ipv4(packet, local_port); + } + + let Some(decrypted) = decrypt_legacy_samp_packet(packet, remote_port) else { + return packet.to_vec(); + }; + + let packet_id = decrypted[0]; + if !*legacy_remote_cipher_active { + if !is_likely_legacy_server_control(packet_id) { + return packet.to_vec(); + } + + *legacy_remote_cipher_active = true; + info!( + "[ipv6-proxy] enabled legacy remote cipher translation (id={}, remote_port={}, local_port={})", + packet_id, remote_port, local_port + ); + } + + encrypt_legacy_samp_packet(&decrypted, local_port) +} + +fn detect_preferred_local_ipv4() -> Option { + let socket = std::net::UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).ok()?; + socket.connect((Ipv4Addr::new(1, 1, 1, 1), 53)).ok()?; + match socket.local_addr().ok()?.ip() { + IpAddr::V4(addr) if !addr.is_loopback() && !addr.is_unspecified() => Some(addr), + _ => None, + } +} + +fn packet_prefix(packet: &[u8]) -> String { + let shown = std::cmp::min(packet.len(), PACKET_PREVIEW_BYTES); + let mut output = String::with_capacity(shown * 2 + 3); + + for byte in &packet[..shown] { + let _ = write!(output, "{:02x}", byte); + } + + if packet.len() > shown { + output.push_str("..."); + } + + output +} + +async fn bind_local_proxy_socket( + bind_ip: Ipv4Addr, + port: u16, +) -> std::result::Result { + let bind_addr = (bind_ip, port); + + for attempt in 1..=LOCAL_BIND_RETRY_COUNT { + match UdpSocket::bind(bind_addr).await { + Ok(socket) => return Ok(socket), + Err(error) if attempt < LOCAL_BIND_RETRY_COUNT => { + warn!( + "[ipv6-proxy] bind retry {}/{} for {}:{} failed: {}", + attempt, LOCAL_BIND_RETRY_COUNT, bind_ip, port, error + ); + sleep(Duration::from_millis(LOCAL_BIND_RETRY_DELAY_MS)).await; + } + Err(error) => { + return Err(format!( + "Failed to bind local IPv4 proxy socket on {}:{} after {} attempts: {}", + bind_ip, port, LOCAL_BIND_RETRY_COUNT, error + )); + } + } + } + + Err(format!( + "Failed to bind local IPv4 proxy socket on {}:{}", + bind_ip, port + )) +} + +#[tauri::command] +pub async fn start_ipv6_proxy( + host: String, + port: i32, + local_port: Option, +) -> std::result::Result { + if !(1..=65535).contains(&port) { + return Err(format!("Invalid port '{}'", port)); + } + + let requested_local_port = local_port.unwrap_or(0); + if !(0..=65535).contains(&requested_local_port) { + return Err(format!("Invalid local port '{}'", requested_local_port)); + } + + info!( + "[ipv6-proxy] start request host={} port={} local_port={}", + host, port, requested_local_port + ); + + let target = resolve_ipv6_target(&host, port as u16) + .await + .map_err(|error| { + let text = String::from(error); + warn!("[ipv6-proxy] target resolution failed: {}", text); + text + })?; + + stop_running_proxy_task().await; + + let local_bind_port = requested_local_port as u16; + let preferred_bind_ip = detect_preferred_local_ipv4().unwrap_or(Ipv4Addr::LOCALHOST); + + let local_socket = match bind_local_proxy_socket(preferred_bind_ip, local_bind_port).await { + Ok(socket) => socket, + Err(error) if preferred_bind_ip != Ipv4Addr::LOCALHOST => { + warn!( + "[ipv6-proxy] {}. Falling back to 127.0.0.1:{}", + error, local_bind_port + ); + bind_local_proxy_socket(Ipv4Addr::LOCALHOST, local_bind_port) + .await + .map_err(|fallback_error| { + warn!("[ipv6-proxy] {}", fallback_error); + fallback_error + })? + } + Err(error) => { + warn!("[ipv6-proxy] {}", error); + return Err(error); + } + }; + let local_addr = local_socket + .local_addr() + .map_err(|e| format!("Failed to read local proxy address: {}", e))?; + let local_port = local_addr.port(); + let local_host = match local_addr.ip() { + IpAddr::V4(ip) => ip, + _ => Ipv4Addr::LOCALHOST, + }; + + let remote_socket = UdpSocket::bind("[::]:0").await.map_err(|e| { + let text = format!("Failed to bind local IPv6 proxy socket: {}", e); + warn!("[ipv6-proxy] {}", text); + text + })?; + remote_socket.connect(target).await.map_err(|e| { + let text = format!("Failed to connect IPv6 proxy socket to {}: {}", target, e); + warn!("[ipv6-proxy] {}", text); + text + })?; + + info!( + "[ipv6-proxy] started local {} -> remote {}", + local_addr, target + ); + + let (stop_tx, mut stop_rx) = oneshot::channel::<()>(); + let task = tokio::spawn(async move { + let mut client_addr: Option = None; + let mut local_buf = [0u8; UDP_BUFFER_SIZE]; + let mut remote_buf = [0u8; UDP_BUFFER_SIZE]; + let mut client_packet_count = 0usize; + let mut remote_packet_count = 0usize; + let mut legacy_client_cipher_active = false; + let mut legacy_remote_cipher_active = false; + + loop { + tokio::select! { + _ = &mut stop_rx => { + info!("[ipv6-proxy] stop requested for remote {}", target); + break; + } + recv = local_socket.recv_from(&mut local_buf) => { + match recv { + Ok((size, source)) => { + client_packet_count += 1; + if client_addr != Some(source) { + info!("[ipv6-proxy] client endpoint {}", source); + client_addr = Some(source); + } + if client_packet_count <= 3 || client_packet_count % 10 == 0 { + info!( + "[ipv6-proxy] client->remote bytes={} prefix={}", + size, + packet_prefix(&local_buf[..size]) + ); + } + + let outbound = rewrite_client_packet( + &local_buf[..size], + target, + local_port, + &mut legacy_client_cipher_active + ); + if let Err(error) = remote_socket.send(&outbound).await { + warn!("[ipv6-proxy] failed to forward client packet to {}: {}", target, error); + break; + } + } + Err(error) => { + warn!("[ipv6-proxy] local recv_from failed: {}", error); + break; + } + } + } + recv = remote_socket.recv(&mut remote_buf) => { + match recv { + Ok(size) => { + remote_packet_count += 1; + let Some(client) = client_addr else { + continue; + }; + if remote_packet_count <= 3 || remote_packet_count % 10 == 0 { + info!( + "[ipv6-proxy] remote->client bytes={} prefix={}", + size, + packet_prefix(&remote_buf[..size]) + ); + } + + let outbound = rewrite_server_packet( + &remote_buf[..size], + local_port, + target.port(), + &mut legacy_remote_cipher_active + ); + if let Err(error) = local_socket.send_to(&outbound, client).await { + warn!("[ipv6-proxy] failed to forward server packet to {}: {}", client, error); + break; + } + } + Err(error) => { + warn!("[ipv6-proxy] remote recv failed: {}", error); + break; + } + } + } + } + } + + info!( + "[ipv6-proxy] relay task stopped for remote {} (client_packets={}, remote_packets={})", + target, client_packet_count, remote_packet_count + ); + }); + + let mut running_proxy = RUNNING_PROXY.lock().await; + *running_proxy = Some(RunningProxy { + stop: stop_tx, + task, + }); + + Ok(ProxyInfo { + host: local_host.to_string(), + port: local_port, + }) +} + +#[tauri::command] +pub async fn stop_ipv6_proxy() -> std::result::Result<(), String> { + stop_running_proxy_task().await; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + decrypt_legacy_samp_packet, encrypt_legacy_samp_packet, LEGACY_OPEN_CONNECTION_REQUEST_NEW, + }; + + #[test] + fn legacy_cipher_roundtrip_uses_port_mask() { + let payload = [LEGACY_OPEN_CONNECTION_REQUEST_NEW, 0x12, 0x2A]; + let port = 55_599; + + let encrypted = encrypt_legacy_samp_packet(&payload, port); + let decrypted = decrypt_legacy_samp_packet(&encrypted, port).expect("decryption should work"); + + assert_eq!(decrypted, payload); + } + + #[test] + fn legacy_cipher_remap_preserves_plain_payload() { + let payload = [LEGACY_OPEN_CONNECTION_REQUEST_NEW, 0x39, 0xDA]; + let local_port = 55_599; + let remote_port = 7_777; + + let encrypted_for_local = encrypt_legacy_samp_packet(&payload, local_port); + let decoded_for_local = decrypt_legacy_samp_packet(&encrypted_for_local, local_port) + .expect("local decryption should work"); + let encrypted_for_remote = encrypt_legacy_samp_packet(&decoded_for_local, remote_port); + let decoded_for_remote = decrypt_legacy_samp_packet(&encrypted_for_remote, remote_port) + .expect("remote decryption should work"); + + assert_eq!(decoded_for_remote, payload); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a0c29f60..d9768f71 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,6 +8,7 @@ mod errors; mod helpers; mod injector; mod ipc; +mod ipv6_proxy; mod query; mod samp; mod validation; @@ -17,6 +18,7 @@ mod validation; mod deeplink; use std::env; +use std::net::IpAddr; use std::process::exit; use std::sync::Mutex; @@ -109,6 +111,36 @@ async fn handle_cli_args() -> Result<()> { if args.has_game_launch_args() { let gamepath = args.gamepath.as_ref().unwrap(); let password = args.get_password(); + let mut launch_host = args.host.as_ref().unwrap().to_string(); + let mut launch_port = args.port.unwrap(); + + let normalized_host = launch_host + .trim() + .trim_start_matches('[') + .trim_end_matches(']') + .to_string(); + let using_ipv6 = normalized_host + .parse::() + .map(|ip| ip.is_ipv6()) + .unwrap_or(false); + + if using_ipv6 { + let proxy = + ipv6_proxy::start_ipv6_proxy(launch_host.clone(), launch_port, Some(0)) + .await + .map_err(LauncherError::Network)?; + launch_host = proxy.host; + launch_port = i32::from(proxy.port); + info!( + "[cli] IPv6 proxy active {}:{} -> {}:{}", + normalized_host.as_str(), + args.port.unwrap(), + launch_host, + launch_port + ); + } else { + let _ = ipv6_proxy::stop_ipv6_proxy().await; + } let omp_client_path = format!( "{}/{}/omp/{}", @@ -124,18 +156,18 @@ async fn handle_cli_args() -> Result<()> { OMP_CLIENT_DLL ); - let omp_path = if args.no_omp { - "" - } else { - &omp_client_path - }; + let omp_path = if args.no_omp { "" } else { &omp_client_path }; run_samp( args.name.as_ref().unwrap(), - args.host.as_ref().unwrap(), - args.port.unwrap(), + &launch_host, + launch_port, gamepath, &format!("{}/{}", gamepath, SAMP_DLL), + "", + false, + "", + 0, omp_path, &password, "", @@ -172,9 +204,13 @@ async fn run_tauri_app() -> Result<()> { commands::inject, commands::get_gtasa_path_from_samp, commands::get_nickname_from_samp, + commands::get_launcher_directory, commands::get_samp_favorite_list, commands::rerun_as_admin, commands::resolve_hostname, + commands::probe_ipv6_query, + ipv6_proxy::start_ipv6_proxy, + ipv6_proxy::stop_ipv6_proxy, commands::is_process_alive, commands::log_info, commands::log_warn, diff --git a/src-tauri/src/query.rs b/src-tauri/src/query.rs index 72e5c839..fd5d07f5 100644 --- a/src-tauri/src/query.rs +++ b/src-tauri/src/query.rs @@ -1,13 +1,13 @@ use actix_web::web::Buf; use byteorder::{LittleEndian, ReadBytesExt}; use once_cell::sync::Lazy; -use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io::{Cursor, Read}; +use std::net::{IpAddr, SocketAddr}; use std::sync::Mutex; +use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -use std::{net::Ipv4Addr, time::Duration}; use tokio::net::{lookup_host, UdpSocket}; use tokio::time::timeout_at; use tokio::time::Instant; @@ -38,8 +38,7 @@ static CACHED_QUERY: Lazy>> = Lazy::new(|| tokio::sync::Mutex::new(None)); pub struct Query { - address: Ipv4Addr, - port: i32, + target: SocketAddr, socket: UdpSocket, } @@ -84,78 +83,62 @@ pub struct ErrorResponse { impl Query { pub async fn new(addr: &str, port: i32) -> Result { - let regex = Regex::new(r"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") - .map_err(|e| LauncherError::Parse(format!("Invalid regex pattern: {}", e)))?; - - let address = match regex.captures(addr) { - Some(_) => { - // it's valid ipv4, move on - addr.to_string() - } - None => { - let socket_addresses = lookup_host(format!("{}:{}", addr, port)).await; - match socket_addresses { - Ok(s) => { - let mut ipv4 = "".to_string(); - for socket_address in s { - if socket_address.is_ipv4() { - // hostname is resolved to ipv4:port, lets split it by ":" and get ipv4 only - let ip_port = socket_address.to_string(); - let vec: Vec<&str> = ip_port.split(':').collect(); - if !vec.is_empty() { - ipv4 = vec[0].to_string(); - break; - } - } - } - if ipv4.is_empty() { - return Err(LauncherError::NotFound( - "No IPv4 address found for hostname".to_string(), - )); - } - ipv4 - } - Err(e) => { - return Err(LauncherError::Network(format!( - "Failed to resolve hostname: {}", - e - ))); - } - } - } + let normalized_addr = addr.trim_start_matches('[').trim_end_matches(']'); + + let target = if normalized_addr.parse::().is_ok() { + SocketAddr::new( + normalized_addr.parse::().map_err(|e| { + LauncherError::InvalidInput(format!("Invalid IP address: {}", e)) + })?, + port as u16, + ) + } else { + let socket_addresses = + lookup_host(format!("{}:{}", addr, port)) + .await + .map_err(|e| { + LauncherError::Network(format!("Failed to resolve hostname: {}", e)) + })?; + socket_addresses.into_iter().next().ok_or_else(|| { + LauncherError::NotFound("No address found for hostname".to_string()) + })? }; - let parsed_address = address - .parse::() - .map_err(|e| LauncherError::InvalidInput(format!("Invalid IP address: {}", e)))?; + let bind_addr = match target { + SocketAddr::V4(_) => "0.0.0.0:0", + SocketAddr::V6(_) => "[::]:0", + }; - let socket = UdpSocket::bind("0.0.0.0:0") + let socket = UdpSocket::bind(bind_addr) .await .map_err(|e| LauncherError::Network(format!("Failed to bind socket: {}", e)))?; socket - .connect(format!("{}:{}", addr, port)) + .connect(target) .await .map_err(|e| LauncherError::Network(format!("Failed to connect to server: {}", e)))?; - let data = Self { - address: parsed_address, - port, - socket, - }; - - Ok(data) + Ok(Self { target, socket }) } pub async fn send(&self, query_type: char) -> Result { let mut packet: Vec = Vec::new(); - packet.extend_from_slice(SAMP_PACKET_HEADER); - for i in 0..4 { - packet.push(self.address.octets()[i]); + match self.target.ip() { + IpAddr::V4(address) => { + packet.extend_from_slice(SAMP_PACKET_HEADER); + packet.extend_from_slice(&address.octets()); + packet.push((self.target.port() & 0xFF) as u8); + packet.push((self.target.port() >> 8 & 0xFF) as u8); + packet.push(query_type as u8); + } + IpAddr::V6(address) => { + packet.extend_from_slice(SAMP6_PACKET_HEADER); + packet.extend_from_slice(&address.octets()); + packet.push((self.target.port() & 0xFF) as u8); + packet.push((self.target.port() >> 8 & 0xFF) as u8); + packet.push(query_type as u8); + } } - packet.push((self.port & 0xFF) as u8); - packet.push((self.port >> 8 & 0xFF) as u8); - packet.push(query_type as u8); if query_type == 'p' { packet.push(0); @@ -189,8 +172,17 @@ impl Query { return Err(LauncherError::Network("No data received".to_string())); } - let query_type = buf[10] as char; - let packet = Cursor::new(buf[11..amt].to_vec()); + let (query_type, payload_offset) = if amt >= 24 && &buf[..5] == SAMP6_PACKET_HEADER { + (buf[23] as char, 24) + } else if amt >= 11 && &buf[..4] == SAMP_PACKET_HEADER { + (buf[10] as char, 11) + } else { + return Err(LauncherError::Network( + "Unknown query response format".to_string(), + )); + }; + + let packet = Cursor::new(buf[payload_offset..amt].to_vec()); match query_type { QUERY_TYPE_INFO => self.build_info_packet(packet), QUERY_TYPE_PLAYERS => self.build_players_packet(packet), diff --git a/src/containers/AddThirdPartyServer/index.tsx b/src/containers/AddThirdPartyServer/index.tsx index c229e866..0e7199dd 100644 --- a/src/containers/AddThirdPartyServer/index.tsx +++ b/src/containers/AddThirdPartyServer/index.tsx @@ -16,11 +16,8 @@ import { useAddThirdPartyServerModal } from "../../states/addThirdPartyServerMod import { usePersistentServers } from "../../states/servers"; import { useTheme } from "../../states/theme"; import { sc } from "../../utils/sizeScaler"; -import { Server } from "../../utils/types"; -import { - isValidDomain, - validateServerAddressIPv4, -} from "../../utils/validation"; +import { getServerEndpoint, Server } from "../../utils/types"; +import { parseServerAddress } from "../../utils/validation"; const AddThirdPartyServerModal = () => { const { visible, showAddThirdPartyServer } = useAddThirdPartyServerModal(); @@ -58,23 +55,11 @@ const AddThirdPartyServerModal = () => { rules: {} as Server["rules"], }; - if (serverAddress.length) { - if (serverAddress.includes(":")) { - const data = serverAddress.split(":"); - serverInfo.ip = data[0]; - serverInfo.port = parseInt(data[1]); - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } else { - if ( - validateServerAddressIPv4(serverAddress) || - isValidDomain(serverAddress) - ) { - serverInfo.ip = serverAddress; - serverInfo.port = 7777; - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } - } - + const parsed = parseServerAddress(serverAddress); + if (parsed) { + serverInfo.ip = parsed.ip; + serverInfo.port = parsed.port; + serverInfo.hostname += ` (${getServerEndpoint(parsed)})`; addToFavorites(serverInfo); showAddThirdPartyServer(false); } @@ -115,7 +100,7 @@ const AddThirdPartyServerModal = () => { { const [visible, showModal] = useState(false); @@ -98,23 +95,11 @@ const ExternalServerHandler = () => { rules: {} as Server["rules"], }; - if (serverAddress.length) { - if (serverAddress.includes(":")) { - const data = serverAddress.split(":"); - serverInfo.ip = data[0]; - serverInfo.port = parseInt(data[1]); - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } else { - if ( - validateServerAddressIPv4(serverAddress) || - isValidDomain(serverAddress) - ) { - serverInfo.ip = serverAddress; - serverInfo.port = 7777; - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } - } - + const parsed = parseServerAddress(serverAddress); + if (parsed) { + serverInfo.ip = parsed.ip; + serverInfo.port = parsed.port; + serverInfo.hostname += ` (${getServerEndpoint(parsed)})`; addToFavorites(serverInfo); showModal(false); } @@ -139,23 +124,11 @@ const ExternalServerHandler = () => { rules: {} as Server["rules"], }; - if (serverAddress.length) { - if (serverAddress.includes(":")) { - const data = serverAddress.split(":"); - serverInfo.ip = data[0]; - serverInfo.port = parseInt(data[1]); - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } else { - if ( - validateServerAddressIPv4(serverAddress) || - isValidDomain(serverAddress) - ) { - serverInfo.ip = serverAddress; - serverInfo.port = 7777; - serverInfo.hostname += ` (${serverInfo.ip}:${serverInfo.port})`; - } - } - + const parsed = parseServerAddress(serverAddress); + if (parsed) { + serverInfo.ip = parsed.ip; + serverInfo.port = parsed.port; + serverInfo.hostname += ` (${getServerEndpoint(parsed)})`; startGame(serverInfo, nickName, gtasaPath, ""); showModal(false); } diff --git a/src/locales/index.ts b/src/locales/index.ts index f4e4ece9..1408fc5f 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -66,6 +66,8 @@ const loadTranslation = (lang: string) => { return import("./translations/ge"); case "fi": return import("./translations/fi"); + case "kr": + return import("./translations/kr"); default: return import("./translations/en"); } @@ -106,7 +108,8 @@ export type LanguageType = | "ta" | "ua" | "ge" - | "fi"; + | "fi" + | "kr"; interface LanguageResource { label: string; @@ -150,6 +153,7 @@ const LANGUAGE_METADATA: Record< ge: { label: "αƒ₯αƒαƒ αƒ—αƒ£αƒšαƒ˜", type: "ge" }, sr: { label: "Брпски", type: "sr" }, fi: { label: "Suomi", type: "fi" }, + kr: { label: "ν•œκ΅­μ–΄", type: "kr" }, }; // Cache for loaded translations @@ -247,3 +251,4 @@ export const changeLanguage = async (lang: LanguageType): Promise => { export default i18n; + diff --git a/src/locales/translations/kr.ts b/src/locales/translations/kr.ts new file mode 100644 index 00000000..75507691 --- /dev/null +++ b/src/locales/translations/kr.ts @@ -0,0 +1,124 @@ +export default { + favorites: "즐겨찾기", + internet: "인터넷", + partners: "νŒŒνŠΈλ„ˆ", + recently_joined: "졜근 μ ‘μ†ν•œ μ„œλ²„", + nickname: "λ‹‰λ„€μž„", + settings: "μ„€μ •", + minimize: "μ΅œμ†Œν™”", + maximize: "μ΅œλŒ€ν™”", + close: "λ‹«κΈ°", + add_server_modal_description_1: "즐겨찾기 λͺ©λ‘μ— μ„œλ²„λ₯Ό 직접 μΆ”κ°€ν•©λ‹ˆλ‹€.", + add_server_modal_description_2: "μ˜ˆμ‹œ: 127.0.0.1:7777", + add: "μΆ”κ°€", + server: "μ„œλ²„", + address: "μ£Όμ†Œ", + players: "ν”Œλ ˆμ΄μ–΄", + server_join_prompt_enter_password: + "이 μ„œλ²„λŠ” λΉ„λ°€λ²ˆν˜Έλ‘œ λ³΄ν˜Έλ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”.", + server_join_prompt_enter_password_input_placeholder: "λΉ„λ°€λ²ˆν˜Έ μž…λ ₯...", + server_join_prompt_nickname_input_placeholder: "λ‹‰λ„€μž„ μž…λ ₯...", + connect: "접속", + copy: "볡사", + remove_from_favorites: "μ¦κ²¨μ°ΎκΈ°μ—μ„œ 제거", + add_to_favorites: "즐겨찾기에 μΆ”κ°€", + filters: "ν•„ν„°", + filter_only_omp_servers: "open.mp μ„œλ²„λ§Œ 보기", + filter_non_empty_servers: "빈 μ„œλ²„ μ œμ™Έ", + filter_unpassworded_servers: "λΉ„λ°€λ²ˆν˜Έ μ—†λŠ” μ„œλ²„λ§Œ 보기", + rule: "κ·œμΉ™", + value: "κ°’", + player: "ν”Œλ ˆμ΄μ–΄", + score: "점수", + locked: "μž κΉ€", + unlocked: "잠금 ν•΄μ œ", + openmp_server: "open.mp μ„œλ²„", + name: "이름", + ping: "ν•‘", + mode: "λͺ¨λ“œ", + filter_servers: "μ„œλ²„ 필터링", + search_for_server_hostname_mode: "μ„œλ²„ 이름 λ˜λŠ” λͺ¨λ“œ 검색", + clear_recently_joined_list: "졜근 접속 λͺ©λ‘ μ΄ˆκΈ°ν™”", + refresh_servers: "μ„œλ²„ λͺ©λ‘ μƒˆλ‘œκ³ μΉ¨", + play: "ν”Œλ ˆμ΄", + remove_selected_server_from_favorites: + "μ„ νƒν•œ μ„œλ²„λ₯Ό μ¦κ²¨μ°ΎκΈ°μ—μ„œ 제거", + add_selected_server_to_favorites: "μ„ νƒν•œ μ„œλ²„λ₯Ό 즐겨찾기에 μΆ”κ°€", + add_server: "μ„œλ²„ μΆ”κ°€", + hide_player_and_rule_list: "ν”Œλ ˆμ΄μ–΄ 및 κ·œμΉ™ λͺ©λ‘ 숨기기", + show_player_and_rule_list: "ν”Œλ ˆμ΄μ–΄ 및 κ·œμΉ™ λͺ©λ‘ 보기", + copy_server_info: "μ„œλ²„ 정보 볡사", + settings_gta_path_input_label: "GTA: San Andreas 경둜", + browse: "찾아보기", + settings_import_nickname_gta_path_from_samp: + "SA-MP μ„€μ •μ—μ„œ λ‹‰λ„€μž„ 및 경둜 뢈러였기", + settings_import_samp_favorite_list: "SA-MP λ°μ΄ν„°μ—μ„œ 즐겨찾기 λͺ©λ‘ 뢈러였기", + settings_reset_application_data: + "μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 데이터 μ΄ˆκΈ°ν™” (μ„€μ • 및 λͺ©λ‘ μ‚­μ œ)", + settings_new_update_available: "⚠ μƒˆλ‘œμš΄ μ—…λ°μ΄νŠΈ κ°€λŠ₯. ν΄λ¦­ν•˜μ—¬ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”! ⚠", + settings_credits_made_by: "μ œμž‘:", + settings_credits_view_source_on_github: "GitHubμ—μ„œ μ†ŒμŠ€ μ½”λ“œ 보기", + update_modal_update_available_title: "μ—…λ°μ΄νŠΈ κ°€λŠ₯!", + update_modal_update_available_description: + 'μƒˆλ‘œμš΄ 런처 λΉŒλ“œλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€!\nν˜„μž¬ 버전: {{ version }}\nμ΅œμ‹  버전: {{ newVersion }}\n"λ‹€μš΄λ‘œλ“œ"λ₯Ό ν΄λ¦­ν•˜μ—¬ 릴리슀 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•˜μ„Έμš”.', + download: "λ‹€μš΄λ‘œλ“œ", + update_modal_remind_me_next_time: "λ‹€μŒμ— μ•Œλ¦Ό", + update_modal_skip_this_update: "이 μ—…λ°μ΄νŠΈ κ±΄λ„ˆλ›°κΈ°", + gta_path_modal_cant_find_game_title: "GTA: San Andreasλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€!", + gta_path_modal_cant_find_game_description: + 'λ‹€μŒ κ²½λ‘œμ—μ„œ GTA: San Andreasλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€:\n - "{{ path }}"\nν•΄λ‹Ή κ²½λ‘œμ— "gta_sa.exe" 파일이 μžˆλŠ”μ§€ 확인해 μ£Όμ„Έμš”.', + open_settings: "μ„€μ • μ—΄κΈ°", + cancel: "μ·¨μ†Œ", + gta_path_modal_cant_find_samp_title: "SA-MPλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€!", + gta_path_modal_cant_find_samp_description: + 'λ‹€μŒ κ²½λ‘œμ—μ„œ SA-MP μ„€μΉ˜ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€:\n - "{{ path }}"\nν•΄λ‹Ή κ²½λ‘œμ— "samp.dll" 파일이 μžˆλŠ”μ§€ 확인해 μ£Όμ„Έμš”.\n', + notification_add_to_favorites_title: "즐겨찾기에 좔가됨!", + notification_add_to_favorites_description: + "{{ server }} μ„œλ²„κ°€ 즐겨찾기 λͺ©λ‘μ— μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", + nickname_modal_name_not_set_title: "λ‹‰λ„€μž„ λ―Έμ„€μ •!", + nickname_modal_name_not_set_description: + "μ„œλ²„μ— μ ‘μ†ν•˜κΈ° 전에 μ‚¬μš©ν•  λ‹‰λ„€μž„μ„ μ„€μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€.", + gta_path_modal_path_not_set_title: "GTA: San Andreas 경둜 λ―Έμ„€μ •!", + gta_path_modal_path_not_set_description: + "GTA: San Andreas κ²½λ‘œκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μ„€μ •μ—μ„œ κ²Œμž„ 폴더λ₯Ό μ§€μ •ν•΄ μ£Όμ„Έμš”.", + admin_permissions_required_modal_title: "κ΄€λ¦¬μž κΆŒν•œ ν•„μš”!", + admin_permissions_required_modal_description: + 'GTA: San Andreasλ₯Ό μ‹€ν–‰ν•˜λ €λ©΄ κ΄€λ¦¬μž κΆŒν•œμ΄ ν•„μš”ν•œ κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€. κ²Œμž„μ΄ "C" λ“œλΌμ΄λΈŒμ— μ„€μΉ˜λœ 경우 등이 원인일 수 μžˆμŠ΅λ‹ˆλ‹€. "κ΄€λ¦¬μž κΆŒν•œμœΌλ‘œ μ‹€ν–‰" λ²„νŠΌμ„ λˆ„λ₯΄κ±°λ‚˜ 직접 런처λ₯Ό κ΄€λ¦¬μž κΆŒν•œμœΌλ‘œ λ‹€μ‹œ μ—΄μ–΄μ£Όμ„Έμš”.', + run_as_admin: "κ΄€λ¦¬μž κΆŒν•œμœΌλ‘œ μ‹€ν–‰", + settings_general_tab_title: "일반", + settings_lang_tab_title: "μ–Έμ–΄", + settings_advanced_tab_title: "κ³ κΈ‰", + settings_advanced_discord_status: "Discord μƒνƒœ ν‘œμ‹œ ν™œμ„±ν™”", + join_discord: "Discord μ„œλ²„ μ°Έμ—¬", + samp_version: "SA-MP 버전", + change_version: "버전 λ³€κ²½", + offline: "μ˜€ν”„λΌμΈ", + from_gtasa_folder: "GTASA ν΄λ”λ‘œλΆ€ν„°", + gta_path_modal_cant_find_samp_description_2: + "λ‹€λ₯Έ 버전을 μ„ νƒν•˜κ±°λ‚˜ SA-MPλ₯Ό 직접 λ‹€μš΄λ‘œλ“œν•˜μ—¬ μ„€μΉ˜ν•΄ μ£Όμ„Έμš”.", + add_or_play_external_server: "즐겨찾기 μΆ”κ°€ λ˜λŠ” ν”Œλ ˆμ΄", + reconnect: "μž¬μ ‘μ†", + settings_advanced_discord_status_requires_restart: + "(μ μš©ν•˜λ €λ©΄ κ²Œμž„μ„ μž¬μ‹œμž‘ν•΄μ•Ό ν•©λ‹ˆλ‹€)", + settings_export_favorite_list_file: "즐겨찾기 λͺ©λ‘ 파일둜 내보내기", + settings_import_favorite_list_file: "즐겨찾기 λͺ©λ‘ νŒŒμΌμ—μ„œ 뢈러였기", + export_no_servers_description: + "내보낼 즐겨찾기 μ„œλ²„κ°€ μ—†μŠ΅λ‹ˆλ‹€.", + export_successful_title: "내보내기 μ™„λ£Œ", + export_successful_description: "μ„œλ²„ λͺ©λ‘μ„ μ„±κ³΅μ μœΌλ‘œ λ‚΄λ³΄λƒˆμŠ΅λ‹ˆλ‹€.", + export_failed_title: "내보내기 μ‹€νŒ¨", + export_failed_description: + "즐겨찾기 μ„œλ²„λ₯Ό λ‚΄λ³΄λ‚΄λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", + import_successful_title: "뢈러였기 μ™„λ£Œ", + import_successful_description: "μ„œλ²„ λͺ©λ‘μ„ μ„±κ³΅μ μœΌλ‘œ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.", + import_failed_title: "뢈러였기 μ‹€νŒ¨", + import_failed_description: + "즐겨찾기 μ„œλ²„λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", + import_invalid_data_description: + "μ„ νƒν•œ νŒŒμΌμ— μœ νš¨ν•˜μ§€ μ•Šμ€ 데이터가 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ˜¬λ°”λ₯Έ 즐겨찾기 λͺ©λ‘ νŒŒμΌμ„ 선택해 μ£Όμ„Έμš”.", + settings_custom_game_exe_label: "μ‚¬μš©μž μ •μ˜ μ‹€ν–‰ 파일(.exe) 이름", + unable_to_find_custom_game_exe_title: + "μ‚¬μš©μž μ •μ˜ μ‹€ν–‰ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€!", + unable_to_find_custom_game_exe_description: + "μ§€μ •λœ μ‚¬μš©μž μ •μ˜ μ‹€ν–‰ 파일이 GTA: San Andreas 디렉토리에 μ—†μŠ΅λ‹ˆλ‹€. μ„€μ • -> κ³ κΈ‰ νƒ­μ—μ„œ 확인해 μ£Όμ„Έμš”.", +}; diff --git a/src/utils/game.ts b/src/utils/game.ts index a0bb3442..3e102bbb 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -19,6 +19,7 @@ import { Log } from "./logger"; import { PING_TIMEOUT_VALUE } from "./query"; import { sc } from "./sizeScaler"; import { Server } from "./types"; +import { isIPv6 } from "./validation"; const showOkModal = (title: string, description: string) => { const { showMessageBox, hideMessageBox } = useMessageBox.getState(); @@ -32,6 +33,33 @@ const showOkModal = (title: string, description: string) => { const getLocalPath = async (...segments: string[]) => path.join(await path.appLocalDataDir(), ...segments); +interface ProxyInfo { + host: string; + port: number; +} + +const stopIpv6ProxySilently = async () => { + try { + await invoke("stop_ipv6_proxy"); + } catch { + // Best effort cleanup. + } +}; + +const startIpv6Proxy = async (host: string, port: number): Promise => { + try { + const proxy = await invoke("start_ipv6_proxy", { + host, + port, + localPort: 0, + }); + return proxy; + } catch (error) { + Log.warn(`[startGame] Failed to start IPv6 proxy for ${host}:${port}`, error); + return null; + } +}; + export const copySharedFilesIntoGameFolder = async () => { const { gtasaPath } = useSettings.getState(); const shared = await getLocalPath("samp", "shared"); @@ -72,15 +100,53 @@ export const startGame = async ( const { sampVersion, customGameExe } = useSettings.getState(); const { showPrompt, setServer } = useJoinServerPrompt.getState(); const { setSelected } = useServers.getState(); + const resolvedAddress = (await getIpAddress(server.ip)) ?? server.ip; + let launchAddress = resolvedAddress; + let launchPort = server.port; + + if (resolvedAddress && isIPv6(resolvedAddress)) { + const normalizedIPv6 = resolvedAddress + .trim() + .replace(/^\[/, "") + .replace(/\]$/, ""); + const proxy = await startIpv6Proxy(normalizedIPv6, server.port); + + if (proxy) { + launchAddress = proxy.host; + launchPort = proxy.port; + Log.info( + `[startGame] IPv6 proxy active ${normalizedIPv6}:${server.port} -> ${launchAddress}:${launchPort}` + ); + } else { + const fallbackIPv4 = await getIpAddress(server.ip, "ipv4"); + if (fallbackIPv4 && !isIPv6(fallbackIPv4)) { + launchAddress = fallbackIPv4; + launchPort = server.port; + await stopIpv6ProxySilently(); + Log.warn( + `[startGame] IPv6 proxy start failed for ${normalizedIPv6}:${server.port}, falling back to IPv4 ${fallbackIPv4}:${server.port}` + ); + } else { + showOkModal( + "IPv6 connection failed", + `Could not start local IPv6 proxy for ${normalizedIPv6}:${server.port} and no IPv4 fallback address is available.` + ); + showPrompt(true); + setServer(server); + return; + } + } + } else { + await stopIpv6ProxySilently(); + } + const connectAddress = launchAddress; if (IN_GAME) { invoke("send_message_to_game", { id: IN_GAME_PROCESS_ID, message: password.length - ? `connect:${await getIpAddress(server.ip)}:${ - server.port - }:${nickname}:${password}` - : `connect:${await getIpAddress(server.ip)}:${server.port}:${nickname}`, + ? `connect:${connectAddress}:${launchPort}:${nickname}:${password}` + : `connect:${connectAddress}:${launchPort}:${nickname}`, }); return; } @@ -221,10 +287,14 @@ export const startGame = async ( invoke("inject", { name: nickname, - ip: await getIpAddress(server.ip), - port: server.port, + ip: launchAddress, + port: launchPort, exe: gtasaPath, dll: ourSAMPDllPath, + traceFile: "", + traceDualstack: false, + traceRemoteIp: "", + traceRemotePort: 0, ompFile: await getLocalPath("omp", "omp-client.dll"), password, customGameExe, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 75a440ab..3b9144d7 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -17,7 +17,12 @@ import { Server, SortType, } from "./types"; -import { validateServerAddressIPv4 } from "./validation"; +import { + isIPv4, + isIPv6, + parseServerAddress, + validateServerAddress, +} from "./validation"; // Server update configuration const SERVER_UPDATE_CONFIG = { @@ -38,8 +43,9 @@ export const mapAPIResponseServerListToAppStructure = ( list: readonly APIResponseServer[] ): Server[] => { return list.map((server): Server => { - const [ip, portStr] = server.core.ip.split(":"); - const port = parseInt(portStr, 10); + const parsed = parseServerAddress(server.core.ip); + const ip = parsed?.ip ?? server.core.ip; + const port = parsed?.port ?? 7777; return { hostname: server.core.hn, @@ -189,20 +195,32 @@ export const fetchUpdateInfo = async () => { // This provides better separation of concerns and reusability export const getIpAddress = async ( - hostname: string + hostname: string, + family?: "ipv4" | "ipv6" ): Promise => { if (!hostname || typeof hostname !== "string") { Log.warn("Invalid hostname provided to getIpAddress:", hostname); return null; } - // Use validation function from validation.ts - if (validateServerAddressIPv4(hostname)) { + const literalIPv4 = isIPv4(hostname); + const literalIPv6 = isIPv6(hostname); + const literalLocalhost = hostname === "localhost"; + + if (literalIPv4 || literalIPv6 || literalLocalhost) { + if (!family) return hostname; + if (family === "ipv4" && literalIPv6) return null; + if (family === "ipv6" && (literalIPv4 || literalLocalhost)) return null; return hostname; } + if (!validateServerAddress(hostname)) { + Log.warn("Invalid server address provided to getIpAddress:", hostname); + return null; + } + try { - const ip = await invoke("resolve_hostname", { hostname }); + const ip = await invoke("resolve_hostname", { hostname, family }); Log.debug(`Resolved ${hostname} to ${ip}`); return ip; } catch (error) { diff --git a/src/utils/types.ts b/src/utils/types.ts index e499bc59..6fa62e9f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,3 +1,5 @@ +import { isIPv6, normalizeIPv6, parseServerAddress } from "./validation"; + // Core game-related types export const RULE_TYPES = [ "artwork", @@ -142,15 +144,19 @@ export const isValidSAMPVersion = (value: string): value is SAMPDLLVersions => { // Helper functions for server operations export const getServerEndpoint = (server: ServerIdentifier): ServerEndpoint => { - return `${server.ip}:${server.port}` as ServerEndpoint; + const host = isIPv6(server.ip) ? `[${normalizeIPv6(server.ip)}]` : server.ip; + return `${host}:${server.port}` as ServerEndpoint; }; export const parseServerEndpoint = ( endpoint: ServerEndpoint ): ServerIdentifier => { - const [ip, portStr] = endpoint.split(":"); - return { - ip, - port: parseInt(portStr, 10), - }; + const parsed = parseServerAddress(endpoint); + if (!parsed) { + return { + ip: endpoint, + port: 7777, + }; + } + return parsed; }; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index b6c0745e..a448f103 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -2,6 +2,8 @@ const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const IPV6_REGEX = + /^(?:\[(?:[0-9A-Fa-f:]+)\]|(?:[0-9A-Fa-f:]+))$/; const DOMAIN_REGEX = /^(?!-)[A-Za-z0-9-]+([-.]{1}[a-z0-9]+)*\.[A-Za-z]{2,6}$/; const WEB_URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&=/]*)$/; @@ -11,6 +13,16 @@ export const isIPv4 = (ip: string): boolean => { return IPV4_REGEX.test(ip); }; +export const normalizeIPv6 = (ip: string): string => { + return ip.trim().replace(/^\[/, "").replace(/\]$/, ""); +}; + +export const isIPv6 = (ip: string): boolean => { + if (!ip || typeof ip !== "string") return false; + const normalized = normalizeIPv6(ip); + return normalized.includes(":") && IPV6_REGEX.test(ip.trim()); +}; + export const isValidDomain = (domain: string): boolean => { if (!domain || typeof domain !== "string") return false; return DOMAIN_REGEX.test(domain); @@ -29,6 +41,15 @@ export const validateServerAddressIPv4 = (address: string): boolean => { return false; }; +export const validateServerAddress = (address: string): boolean => { + if (!address || typeof address !== "string") return false; + return ( + validateServerAddressIPv4(address) || + isIPv6(address) || + isValidDomain(address) + ); +}; + export const validateWebUrl = (url: string): boolean => { if (!url || typeof url !== "string") return false; @@ -57,5 +78,56 @@ export const validateServerEndpoint = ( ip: string, port: number | string ): boolean => { - return validateServerAddressIPv4(ip) && validatePort(port); + return validateServerAddress(ip) && validatePort(port); +}; + +export interface ParsedServerAddress { + ip: string; + port: number; +} + +export const parseServerAddress = ( + address: string, + defaultPort: number = 7777 +): ParsedServerAddress | null => { + if (!address || typeof address !== "string") return null; + + const trimmed = address.trim(); + if (!trimmed) return null; + + if (trimmed.startsWith("[")) { + const closingBracket = trimmed.indexOf("]"); + if (closingBracket === -1) return null; + + const host = trimmed.slice(1, closingBracket); + const suffix = trimmed.slice(closingBracket + 1); + if (!isIPv6(host)) return null; + + if (!suffix) { + return { ip: host, port: defaultPort }; + } + + if (!suffix.startsWith(":")) return null; + const port = parseInt(suffix.slice(1), 10); + return validatePort(port) ? { ip: host, port } : null; + } + + if (isIPv6(trimmed)) { + return { ip: normalizeIPv6(trimmed), port: defaultPort }; + } + + const separatorIndex = trimmed.lastIndexOf(":"); + if (separatorIndex !== -1) { + const host = trimmed.slice(0, separatorIndex); + const port = parseInt(trimmed.slice(separatorIndex + 1), 10); + if (validateServerAddress(host) && validatePort(port)) { + return { ip: host, port }; + } + } + + if (validateServerAddress(trimmed)) { + return { ip: trimmed, port: defaultPort }; + } + + return null; }; diff --git a/tools/win-socket-trace/CMakeLists.txt b/tools/win-socket-trace/CMakeLists.txt new file mode 100644 index 00000000..ffe4b7d9 --- /dev/null +++ b/tools/win-socket-trace/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16) +project(omp_socket_trace LANGUAGES CXX) + +add_library(omp-socket-trace SHARED wsock_trace.cpp) +set_target_properties(omp-socket-trace PROPERTIES + OUTPUT_NAME "omp-socket-trace" + PREFIX "" +) + +target_compile_features(omp-socket-trace PRIVATE cxx_std_17) +target_link_libraries(omp-socket-trace PRIVATE ws2_32) + +if(MINGW) + target_link_options(omp-socket-trace PRIVATE -static-libgcc -static-libstdc++) +endif() diff --git a/tools/win-socket-trace/README.md b/tools/win-socket-trace/README.md new file mode 100644 index 00000000..3787ded5 --- /dev/null +++ b/tools/win-socket-trace/README.md @@ -0,0 +1,57 @@ +# omp-socket-trace + +Small Windows DLL used for runtime network diagnostics in the GTA/SA-MP process. + +It patches the Import Address Table (IAT) of `samp.dll` (and main module) for: + +- `socket` +- `WSASocketA` +- `connect` +- `WSAConnect` +- `sendto` +- `recvfrom` + +The DLL writes lines to `omp_socket_trace.log` in the GTA executable directory. + +## Why this helps + +For your IPv6 issue, this answers three hard questions immediately: + +- Does the client call `connect`/`sendto` at all during join? +- Which socket family is used (`AF_INET` vs `AF_INET6`)? +- Which destination address/port is passed from the game stack? + +## Build (Windows, MinGW 32-bit recommended) + +GTA/SA-MP is 32-bit, so build this DLL as 32-bit. + +### CMake + +```bash +cmake -S . -B build-mingw32 -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build build-mingw32 -j +``` + +### Direct g++ + +```bash +i686-w64-mingw32-g++ -O2 -std=c++17 -shared -static-libgcc -static-libstdc++ -o omp-socket-trace.dll wsock_trace.cpp -lws2_32 +``` + +## Launcher integration + +The launcher code in this repo was extended to inject this DLL optionally. + +- Put `omp-socket-trace.dll` into launcher local data path: `.../omp/omp-socket-trace.dll` +- If present, it gets injected before `samp.dll` +- If missing, launcher behaves unchanged + +## Expected log examples + +```text +... socket(af=AF_INET6/23,type=2,proto=17) => ... +... connect(sock=...,family=AF_INET6/23,target=[2001:...]:7777) => rc=... +... sendto(sock=...,family=AF_INET6/23,to=[2001:...]:7777,len=...) +``` + +If only `AF_INET` appears in a failed IPv6 test, the blocker is in client-side address/socket handling. diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp new file mode 100644 index 00000000..eea8134b --- /dev/null +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -0,0 +1,1622 @@ +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct SocketMeta { + int family = AF_UNSPEC; + bool forced_dualstack = false; +}; + +using socket_fn_t = SOCKET(WSAAPI*)(int, int, int); +using wsasocketa_fn_t = SOCKET(WSAAPI*)(int, int, int, LPWSAPROTOCOL_INFOA, GROUP, DWORD); +using wsasocketw_fn_t = SOCKET(WSAAPI*)(int, int, int, LPWSAPROTOCOL_INFOW, GROUP, DWORD); +using closesocket_fn_t = int(WSAAPI*)(SOCKET); +using connect_fn_t = int(WSAAPI*)(SOCKET, const sockaddr*, int); +using wsaconnect_fn_t = + int(WSAAPI*)(SOCKET, const sockaddr*, int, LPWSABUF, LPWSABUF, LPQOS, LPQOS); +using bind_fn_t = int(WSAAPI*)(SOCKET, const sockaddr*, int); +using sendto_fn_t = int(WSAAPI*)(SOCKET, const char*, int, int, const sockaddr*, int); +using recv_fn_t = int(WSAAPI*)(SOCKET, char*, int, int); +using recvfrom_fn_t = int(WSAAPI*)(SOCKET, char*, int, int, sockaddr*, int*); +using wsarecvex_fn_t = int(WSAAPI*)(SOCKET, char*, int, int*); +using wsarecv_fn_t = int(WSAAPI*)( + SOCKET, + LPWSABUF, + DWORD, + LPDWORD, + LPDWORD, + LPWSAOVERLAPPED, + LPWSAOVERLAPPED_COMPLETION_ROUTINE); +using wsarecvfrom_fn_t = int(WSAAPI*)( + SOCKET, + LPWSABUF, + DWORD, + LPDWORD, + LPDWORD, + sockaddr*, + LPINT, + LPWSAOVERLAPPED, + LPWSAOVERLAPPED_COMPLETION_ROUTINE); +using getsockname_fn_t = int(WSAAPI*)(SOCKET, sockaddr*, int*); +using getpeername_fn_t = int(WSAAPI*)(SOCKET, sockaddr*, int*); +using getprocaddress_fn_t = FARPROC(WINAPI*)(HMODULE, LPCSTR); +using loadlibrarya_fn_t = HMODULE(WINAPI*)(LPCSTR); + +static socket_fn_t g_real_socket = nullptr; +static wsasocketa_fn_t g_real_wsasocketa = nullptr; +static wsasocketw_fn_t g_real_wsasocketw = nullptr; +static closesocket_fn_t g_real_closesocket = nullptr; +static connect_fn_t g_real_connect = nullptr; +static wsaconnect_fn_t g_real_wsaconnect = nullptr; +static bind_fn_t g_real_bind = nullptr; +static sendto_fn_t g_real_sendto = nullptr; +static recv_fn_t g_real_recv = nullptr; +static recvfrom_fn_t g_real_recvfrom = nullptr; +static wsarecvex_fn_t g_real_wsarecvex = nullptr; +static wsarecv_fn_t g_real_wsarecv = nullptr; +static wsarecvfrom_fn_t g_real_wsarecvfrom = nullptr; +static getsockname_fn_t g_real_getsockname = nullptr; +static getpeername_fn_t g_real_getpeername = nullptr; +static getprocaddress_fn_t g_real_getprocaddress = nullptr; +static loadlibrarya_fn_t g_real_loadlibrarya = nullptr; + +static std::mutex g_log_mutex; +static std::once_flag g_log_once; +static std::string g_log_path; + +static std::mutex g_socket_mutex; +static std::unordered_map g_socket_meta; +static std::mutex g_patch_log_mutex; +static std::unordered_set g_patch_log_seen; + +static std::atomic g_initialized{false}; +static bool g_dualstack_enabled = false; +static bool g_force_remote_enabled = false; +static sockaddr_in6 g_forced_remote_addr{}; +static HMODULE g_self_module = nullptr; + +static const char* family_name(int af) { + switch (af) { + case AF_INET: + return "AF_INET"; + case AF_INET6: + return "AF_INET6"; + case AF_UNSPEC: + return "AF_UNSPEC"; + default: + return "AF_OTHER"; + } +} + +static std::string get_env_string(const char* key) { + DWORD needed = GetEnvironmentVariableA(key, nullptr, 0); + if (needed == 0) { + return {}; + } + std::string out; + out.resize(needed); + DWORD rc = GetEnvironmentVariableA(key, out.data(), needed); + if (rc == 0 || rc >= needed) { + return {}; + } + out.resize(rc); + return out; +} + +static bool get_env_bool(const char* key) { + std::string v = get_env_string(key); + if (v.empty()) { + return false; + } + std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return (char)tolower(c); }); + return v == "1" || v == "true" || v == "yes" || v == "on"; +} + +static bool parse_u16(const std::string& value, unsigned short* out) { + if (!out || value.empty()) { + return false; + } + + char* end = nullptr; + unsigned long parsed = std::strtoul(value.c_str(), &end, 10); + if (end == value.c_str() || *end != '\0' || parsed > 65535) { + return false; + } + + *out = static_cast(parsed); + return true; +} + +static std::string normalize_ipv6(std::string value) { + while (!value.empty() && value.front() == '[') { + value.erase(value.begin()); + } + while (!value.empty() && value.back() == ']') { + value.pop_back(); + } + return value; +} + +static void ensure_directory_path(std::string path) { + if (path.empty()) { + return; + } + + for (char& c : path) { + if (c == '/') { + c = '\\'; + } + } + + size_t pos = 0; + if (path.size() >= 2 && path[1] == ':') { + pos = 3; + } else if (path.size() >= 2 && path[0] == '\\' && path[1] == '\\') { + pos = 2; + } + + while (pos < path.size()) { + size_t next = path.find('\\', pos); + if (next == std::string::npos) { + break; + } + + std::string piece = path.substr(0, next); + if (!piece.empty()) { + CreateDirectoryA(piece.c_str(), nullptr); + } + pos = next + 1; + } +} + +static void init_log_path() { + std::call_once(g_log_once, [] { + std::string custom = get_env_string("OMP_TRACE_LOG"); + if (!custom.empty()) { + g_log_path = custom; + } else { + std::string local = get_env_string("LOCALAPPDATA"); + if (!local.empty()) { + g_log_path = local + "\\mp.open.launcher\\omp\\omp_socket_trace.log"; + } else { + g_log_path = "C:\\temp\\omp_socket_trace.log"; + } + } + + size_t slash = g_log_path.find_last_of("\\/"); + if (slash != std::string::npos) { + ensure_directory_path(g_log_path.substr(0, slash)); + } + }); +} + +static void log_line(const char* fmt, ...) { + init_log_path(); + + char msg[2048]; + va_list ap; + va_start(ap, fmt); + _vsnprintf(msg, sizeof(msg) - 1, fmt, ap); + va_end(ap); + msg[sizeof(msg) - 1] = '\0'; + + SYSTEMTIME st; + GetLocalTime(&st); + + char line[2300]; + _snprintf( + line, + sizeof(line) - 1, + "%04u-%02u-%02u %02u:%02u:%02u.%03u pid=%lu tid=%lu %s\n", + (unsigned)st.wYear, + (unsigned)st.wMonth, + (unsigned)st.wDay, + (unsigned)st.wHour, + (unsigned)st.wMinute, + (unsigned)st.wSecond, + (unsigned)st.wMilliseconds, + (unsigned long)GetCurrentProcessId(), + (unsigned long)GetCurrentThreadId(), + msg); + line[sizeof(line) - 1] = '\0'; + + { + std::lock_guard lock(g_log_mutex); + FILE* f = fopen(g_log_path.c_str(), "ab"); + if (f) { + fwrite(line, 1, strlen(line), f); + fclose(f); + } + } + + OutputDebugStringA(line); +} + +static std::string format_sockaddr(const sockaddr* sa, int salen) { + if (!sa) { + return "(null)"; + } + + char ipbuf[INET6_ADDRSTRLEN] = {0}; + if (sa->sa_family == AF_INET && salen >= (int)sizeof(sockaddr_in)) { + auto* in4 = reinterpret_cast(sa); + if (!InetNtopA(AF_INET, (PVOID)&in4->sin_addr, ipbuf, sizeof(ipbuf))) { + strcpy(ipbuf, "?"); + } + char out[128]; + _snprintf(out, sizeof(out) - 1, "%s:%u", ipbuf, (unsigned)ntohs(in4->sin_port)); + out[sizeof(out) - 1] = '\0'; + return out; + } + + if (sa->sa_family == AF_INET6 && salen >= (int)sizeof(sockaddr_in6)) { + auto* in6 = reinterpret_cast(sa); + if (!InetNtopA(AF_INET6, (PVOID)&in6->sin6_addr, ipbuf, sizeof(ipbuf))) { + strcpy(ipbuf, "?"); + } + char out[192]; + if (in6->sin6_scope_id != 0) { + _snprintf( + out, + sizeof(out) - 1, + "[%s%%%u]:%u", + ipbuf, + (unsigned)in6->sin6_scope_id, + (unsigned)ntohs(in6->sin6_port)); + } else { + _snprintf(out, sizeof(out) - 1, "[%s]:%u", ipbuf, (unsigned)ntohs(in6->sin6_port)); + } + out[sizeof(out) - 1] = '\0'; + return out; + } + + char out[64]; + _snprintf(out, sizeof(out) - 1, "family=%d,len=%d", sa->sa_family, salen); + out[sizeof(out) - 1] = '\0'; + return out; +} + +static bool is_ordinal_proc_name(LPCSTR name) { + return reinterpret_cast(name) <= 0xFFFF; +} + +static const char* module_basename(const char* path) { + if (!path) { + return ""; + } + + const char* slash = strrchr(path, '\\'); + const char* slash2 = strrchr(path, '/'); + const char* base = slash ? slash + 1 : path; + if (slash2 && slash2 > slash) { + base = slash2 + 1; + } + return base; +} + +static std::string module_name_for_handle(HMODULE mod) { + if (!mod) { + return "(null)"; + } + + char path[MAX_PATH * 2] = {0}; + DWORD len = GetModuleFileNameA(mod, path, sizeof(path)); + if (len == 0 || len >= sizeof(path)) { + return "(unknown)"; + } + return module_basename(path); +} + +static std::string format_bytes_prefix(const unsigned char* data, size_t len, size_t full_len = 0) { + if (!data || len == 0) { + return "-"; + } + if (full_len == 0) { + full_len = len; + } + + static const char kHex[] = "0123456789abcdef"; + const size_t shown = std::min(len, 12); + std::string out; + out.reserve(shown * 2 + (len > shown ? 3 : 0)); + for (size_t i = 0; i < shown; ++i) { + unsigned char byte = data[i]; + out.push_back(kHex[byte >> 4]); + out.push_back(kHex[byte & 0x0F]); + } + if (full_len > shown) { + out += "..."; + } + return out; +} + +static std::string format_buffer_prefix(const char* data, size_t len) { + return format_bytes_prefix(reinterpret_cast(data), len); +} + +static std::string format_wsabuf_prefix(LPWSABUF buffers, DWORD buffer_count, size_t total_len) { + if (!buffers || buffer_count == 0 || total_len == 0) { + return "-"; + } + + unsigned char preview[12] = {0}; + size_t copied = 0; + size_t remaining = total_len; + for (DWORD i = 0; i < buffer_count && copied < sizeof(preview) && remaining > 0; ++i) { + if (!buffers[i].buf || buffers[i].len == 0) { + continue; + } + size_t chunk = std::min(buffers[i].len, remaining); + size_t take = std::min(chunk, sizeof(preview) - copied); + if (take > 0) { + memcpy(preview + copied, buffers[i].buf, take); + copied += take; + } + remaining -= chunk; + } + + if (copied == 0) { + return "-"; + } + + return format_bytes_prefix(preview, copied, total_len); +} + +static bool module_name_equals(HMODULE mod, const char* expected) { + std::string name = module_name_for_handle(mod); + return _stricmp(name.c_str(), expected) == 0; +} + +static bool is_ws2_family_module(HMODULE mod) { + return module_name_equals(mod, "ws2_32.dll") || module_name_equals(mod, "wsock32.dll") || + module_name_equals(mod, "mswsock.dll"); +} + +static bool is_v4_mapped(const sockaddr_in6& in6) { + const unsigned char* b = reinterpret_cast(&in6.sin6_addr); + for (int i = 0; i < 10; ++i) { + if (b[i] != 0) { + return false; + } + } + return b[10] == 0xff && b[11] == 0xff; +} + +static bool is_v4_any(const sockaddr_in& in4) { + return in4.sin_addr.s_addr == htonl(INADDR_ANY); +} + +static bool is_v4_loopback(const sockaddr_in& in4) { + return ntohl(in4.sin_addr.s_addr) == INADDR_LOOPBACK; +} + +static bool is_v6_any(const in6_addr& in6) { + static const unsigned char kZero[16] = {0}; + return memcmp(&in6, kZero, sizeof(kZero)) == 0; +} + +static bool is_v6_loopback(const in6_addr& in6) { + static const unsigned char kLoopback[16] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + return memcmp(&in6, kLoopback, sizeof(kLoopback)) == 0; +} + +static sockaddr_in6 v4_to_mapped(const sockaddr_in& in4) { + sockaddr_in6 out{}; + out.sin6_family = AF_INET6; + out.sin6_port = in4.sin_port; + unsigned char* b = reinterpret_cast(&out.sin6_addr); + b[10] = 0xff; + b[11] = 0xff; + memcpy(&b[12], &in4.sin_addr, 4); + return out; +} + +static sockaddr_in6 v4_bind_to_v6(const sockaddr_in& in4) { + sockaddr_in6 out{}; + out.sin6_family = AF_INET6; + out.sin6_port = in4.sin_port; + + if (is_v4_any(in4)) { + return out; + } + + if (is_v4_loopback(in4)) { + unsigned char* b = reinterpret_cast(&out.sin6_addr); + b[15] = 1; + return out; + } + + return v4_to_mapped(in4); +} + +static bool mapped_to_v4(const sockaddr_in6& in6, sockaddr_in* out4) { + if (!is_v4_mapped(in6)) { + return false; + } + sockaddr_in out{}; + out.sin_family = AF_INET; + out.sin_port = in6.sin6_port; + const unsigned char* b = reinterpret_cast(&in6.sin6_addr); + memcpy(&out.sin_addr, &b[12], 4); + *out4 = out; + return true; +} + +static bool native_v6_to_v4_compat( + const sockaddr_in6& in6, + sockaddr_in* out4, + bool peer_addr) { + if (!out4) { + return false; + } + + sockaddr_in out{}; + out.sin_family = AF_INET; + out.sin_port = in6.sin6_port; + + if (is_v6_any(in6.sin6_addr)) { + out.sin_addr.s_addr = htonl(INADDR_ANY); + *out4 = out; + return true; + } + + if (is_v6_loopback(in6.sin6_addr) || (peer_addr && g_force_remote_enabled)) { + out.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + *out4 = out; + return true; + } + + return false; +} + +static void remember_socket(SOCKET s, int family, bool forced_dualstack) { + if (s == INVALID_SOCKET) { + return; + } + std::lock_guard lock(g_socket_mutex); + g_socket_meta[s] = SocketMeta{family, forced_dualstack}; +} + +static void forget_socket(SOCKET s) { + std::lock_guard lock(g_socket_mutex); + g_socket_meta.erase(s); +} + +static bool get_socket_meta(SOCKET s, SocketMeta* out) { + std::lock_guard lock(g_socket_mutex); + auto it = g_socket_meta.find(s); + if (it == g_socket_meta.end()) { + return false; + } + if (out) { + *out = it->second; + } + return true; +} + +static bool should_translate_to_v6(SOCKET s) { + SocketMeta m{}; + return g_dualstack_enabled && get_socket_meta(s, &m) && m.forced_dualstack; +} + +static bool copy_or_translate_addr( + const sockaddr_storage& src, + int src_len, + sockaddr* dst, + int* dst_len, + bool peer_addr) { + if (!dst || !dst_len || *dst_len <= 0) { + return false; + } + + if (src.ss_family == AF_INET6 && src_len >= (int)sizeof(sockaddr_in6) && + *dst_len >= (int)sizeof(sockaddr_in)) { + sockaddr_in out4{}; + const sockaddr_in6& in6 = *reinterpret_cast(&src); + if (mapped_to_v4(in6, &out4) || native_v6_to_v4_compat(in6, &out4, peer_addr)) { + memcpy(dst, &out4, sizeof(out4)); + *dst_len = (int)sizeof(out4); + return true; + } + } + + int to_copy = std::min(*dst_len, src_len); + memcpy(dst, &src, to_copy); + *dst_len = to_copy; + return true; +} + +template +static void resolve_real(T& fn, HMODULE ws2, const char* name) { + if (!fn && ws2) { + FARPROC proc = g_real_getprocaddress ? g_real_getprocaddress(ws2, name) : ::GetProcAddress(ws2, name); + fn = reinterpret_cast(proc); + } +} + +static void resolve_kernel_functions() { + HMODULE kernel = GetModuleHandleA("kernel32.dll"); + if (!kernel) { + kernel = LoadLibraryA("kernel32.dll"); + } + if (!kernel) { + return; + } + + if (!g_real_getprocaddress) { + g_real_getprocaddress = + reinterpret_cast(::GetProcAddress(kernel, "GetProcAddress")); + } + + FARPROC(WINAPI *resolve_proc)(HMODULE, LPCSTR) = g_real_getprocaddress ? g_real_getprocaddress : ::GetProcAddress; + + resolve_real(g_real_loadlibrarya, kernel, "LoadLibraryA"); + + if (!g_real_loadlibrarya) { + g_real_loadlibrarya = reinterpret_cast(resolve_proc(kernel, "LoadLibraryA")); + } +} + +static void resolve_real_functions() { + resolve_kernel_functions(); + HMODULE ws2 = GetModuleHandleA("ws2_32.dll"); + if (!ws2) { + ws2 = g_real_loadlibrarya ? g_real_loadlibrarya("ws2_32.dll") : LoadLibraryA("ws2_32.dll"); + } + HMODULE mswsock = GetModuleHandleA("mswsock.dll"); + if (!mswsock) { + mswsock = g_real_loadlibrarya ? g_real_loadlibrarya("mswsock.dll") : LoadLibraryA("mswsock.dll"); + } + resolve_real(g_real_socket, ws2, "socket"); + resolve_real(g_real_wsasocketa, ws2, "WSASocketA"); + resolve_real(g_real_wsasocketw, ws2, "WSASocketW"); + resolve_real(g_real_closesocket, ws2, "closesocket"); + resolve_real(g_real_connect, ws2, "connect"); + resolve_real(g_real_wsaconnect, ws2, "WSAConnect"); + resolve_real(g_real_bind, ws2, "bind"); + resolve_real(g_real_sendto, ws2, "sendto"); + resolve_real(g_real_recv, ws2, "recv"); + resolve_real(g_real_recvfrom, ws2, "recvfrom"); + resolve_real(g_real_wsarecvex, mswsock, "WSARecvEx"); + resolve_real(g_real_wsarecv, ws2, "WSARecv"); + resolve_real(g_real_wsarecvfrom, ws2, "WSARecvFrom"); + resolve_real(g_real_getsockname, ws2, "getsockname"); + resolve_real(g_real_getpeername, ws2, "getpeername"); +} + +static void log_patch_once( + HMODULE mod, + const char* import_name, + const char* func_name, + bool by_ordinal) { + if (!import_name || !func_name) { + return; + } + + std::string module_name = module_name_for_handle(mod); + std::string key = module_name; + key.append("|"); + key.append(import_name); + key.append("|"); + key.append(func_name); + key.append(by_ordinal ? "|ord" : "|name"); + + { + std::lock_guard lock(g_patch_log_mutex); + if (!g_patch_log_seen.insert(key).second) { + return; + } + } + + log_line( + "hook-patch module=%s import=%s symbol=%s mode=%s", + module_name.c_str(), + import_name, + func_name, + by_ordinal ? "ordinal" : "name"); +} + +static bool patch_iat_for_module( + HMODULE mod, + const char* import_name, + const char* func_name, + void* replacement, + void** original_store) { + if (!mod) { + return false; + } + + auto base = reinterpret_cast(mod); + auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) { + return false; + } + + auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) { + return false; + } + + auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (!dir.VirtualAddress) { + return false; + } + + auto* imp = reinterpret_cast(base + dir.VirtualAddress); + bool patched = false; + + for (; imp->Name; ++imp) { + auto* dll = reinterpret_cast(base + imp->Name); + if (_stricmp(dll, import_name) != 0) { + continue; + } + + void* import_symbol = nullptr; + HMODULE import_module = GetModuleHandleA(dll); + if (import_module) { + FARPROC proc = + g_real_getprocaddress ? g_real_getprocaddress(import_module, func_name) + : ::GetProcAddress(import_module, func_name); + import_symbol = reinterpret_cast(proc); + if (original_store && *original_store == nullptr && import_symbol) { + *original_store = import_symbol; + } + } + + auto* thunk = reinterpret_cast(base + imp->FirstThunk); + auto* orig = reinterpret_cast( + base + (imp->OriginalFirstThunk ? imp->OriginalFirstThunk : imp->FirstThunk)); + + for (; orig->u1.AddressOfData; ++orig, ++thunk) { + auto current = reinterpret_cast((uintptr_t)thunk->u1.Function); + bool matched_by_ordinal = false; + if (IMAGE_SNAP_BY_ORDINAL(orig->u1.Ordinal)) { + if (current == replacement) { + continue; + } + + bool ordinal_match = false; + if (original_store && *original_store != nullptr && current == *original_store) { + ordinal_match = true; + } else if (import_symbol && current == import_symbol) { + ordinal_match = true; + } + + if (!ordinal_match) { + continue; + } + matched_by_ordinal = true; + } else { + auto* by_name = + reinterpret_cast(base + orig->u1.AddressOfData); + if (strcmp(reinterpret_cast(by_name->Name), func_name) != 0) { + continue; + } + } + + if (current == replacement) { + patched = true; + continue; + } + + DWORD old_protect = 0; + if (!VirtualProtect( + &thunk->u1.Function, + sizeof(thunk->u1.Function), + PAGE_EXECUTE_READWRITE, + &old_protect)) { + continue; + } + + if (original_store && *original_store == nullptr) { + *original_store = current; + } + thunk->u1.Function = (uintptr_t)replacement; + + DWORD ignored = 0; + VirtualProtect( + &thunk->u1.Function, + sizeof(thunk->u1.Function), + old_protect, + &ignored); + FlushInstructionCache( + GetCurrentProcess(), + &thunk->u1.Function, + sizeof(thunk->u1.Function)); + patched = true; + log_patch_once(mod, dll, func_name, matched_by_ordinal); + } + } + + return patched; +} + +struct HookDef { + const char* symbol; + void* replacement; + void** original; +}; + +static SOCKET WSAAPI hook_socket(int af, int type, int protocol); +static SOCKET WSAAPI hook_wsasocketa( + int af, + int type, + int protocol, + LPWSAPROTOCOL_INFOA info, + GROUP group, + DWORD flags); +static SOCKET WSAAPI hook_wsasocketw( + int af, + int type, + int protocol, + LPWSAPROTOCOL_INFOW info, + GROUP group, + DWORD flags); +static int WSAAPI hook_closesocket(SOCKET s); +static int WSAAPI hook_connect(SOCKET s, const sockaddr* name, int namelen); +static int WSAAPI hook_wsaconnect( + SOCKET s, + const sockaddr* name, + int namelen, + LPWSABUF callerData, + LPWSABUF calleeData, + LPQOS sqos, + LPQOS gqos); +static int WSAAPI hook_bind(SOCKET s, const sockaddr* name, int namelen); +static int WSAAPI hook_sendto( + SOCKET s, + const char* buf, + int len, + int flags, + const sockaddr* to, + int tolen); +static int WSAAPI hook_recvfrom( + SOCKET s, + char* buf, + int len, + int flags, + sockaddr* from, + int* fromlen); +static int WSAAPI hook_recv(SOCKET s, char* buf, int len, int flags); +static int WSAAPI hook_wsarecv( + SOCKET s, + LPWSABUF buffers, + DWORD buffer_count, + LPDWORD bytes_received, + LPDWORD flags, + LPWSAOVERLAPPED overlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE completion_routine); +static int WSAAPI hook_wsarecvfrom( + SOCKET s, + LPWSABUF buffers, + DWORD buffer_count, + LPDWORD bytes_received, + LPDWORD flags, + sockaddr* from, + LPINT fromlen, + LPWSAOVERLAPPED overlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE completion_routine); +static int WSAAPI hook_wsarecvex(SOCKET s, char* buf, int len, int* flags); +static int WSAAPI hook_getsockname(SOCKET s, sockaddr* name, int* namelen); +static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen); +static FARPROC WINAPI hook_getprocaddress(HMODULE mod, LPCSTR proc_name); + +static HookDef kSocketHooks[] = { + {"socket", (void*)&hook_socket, (void**)&g_real_socket}, + {"WSASocketA", (void*)&hook_wsasocketa, (void**)&g_real_wsasocketa}, + {"WSASocketW", (void*)&hook_wsasocketw, (void**)&g_real_wsasocketw}, + {"closesocket", (void*)&hook_closesocket, (void**)&g_real_closesocket}, + {"connect", (void*)&hook_connect, (void**)&g_real_connect}, + {"WSAConnect", (void*)&hook_wsaconnect, (void**)&g_real_wsaconnect}, + {"bind", (void*)&hook_bind, (void**)&g_real_bind}, + {"sendto", (void*)&hook_sendto, (void**)&g_real_sendto}, + {"recv", (void*)&hook_recv, (void**)&g_real_recv}, + {"recvfrom", (void*)&hook_recvfrom, (void**)&g_real_recvfrom}, + {"WSARecvEx", (void*)&hook_wsarecvex, (void**)&g_real_wsarecvex}, + {"WSARecv", (void*)&hook_wsarecv, (void**)&g_real_wsarecv}, + {"WSARecvFrom", (void*)&hook_wsarecvfrom, (void**)&g_real_wsarecvfrom}, + {"getsockname", (void*)&hook_getsockname, (void**)&g_real_getsockname}, + {"getpeername", (void*)&hook_getpeername, (void**)&g_real_getpeername}, +}; + +static HookDef kKernelHooks[] = { + {"GetProcAddress", (void*)&hook_getprocaddress, (void**)&g_real_getprocaddress}, +}; + +static const HookDef* find_socket_hook(const char* proc_name) { + if (!proc_name) { + return nullptr; + } + + for (size_t i = 0; i < sizeof(kSocketHooks) / sizeof(kSocketHooks[0]); ++i) { + if (strcmp(proc_name, kSocketHooks[i].symbol) == 0) { + return &kSocketHooks[i]; + } + } + return nullptr; +} + +static void apply_hooks_for_module(HMODULE mod) { + for (const auto& hook : kSocketHooks) { + patch_iat_for_module(mod, "ws2_32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "WS2_32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "wsock32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "WSOCK32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "mswsock.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "MSWSOCK.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "MSWSOCK.DLL", hook.symbol, hook.replacement, hook.original); + } + + if (mod == g_self_module) { + return; + } + + for (const auto& hook : kKernelHooks) { + patch_iat_for_module(mod, "kernel32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "KERNEL32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "Kernel32.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "kernelbase.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "KERNELBASE.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module(mod, "KernelBase.dll", hook.symbol, hook.replacement, hook.original); + patch_iat_for_module( + mod, + "api-ms-win-core-libraryloader-l1-1-0.dll", + hook.symbol, + hook.replacement, + hook.original); + patch_iat_for_module( + mod, + "api-ms-win-core-libraryloader-l1-2-0.dll", + hook.symbol, + hook.replacement, + hook.original); + patch_iat_for_module( + mod, + "api-ms-win-core-libraryloader-l1-2-1.dll", + hook.symbol, + hook.replacement, + hook.original); + } +} + +static void patch_loaded_modules() { + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) { + return; + } + + MODULEENTRY32 me{}; + me.dwSize = sizeof(me); + if (Module32First(snap, &me)) { + do { + apply_hooks_for_module(me.hModule); + } while (Module32Next(snap, &me)); + } + CloseHandle(snap); +} + +static FARPROC WINAPI hook_getprocaddress(HMODULE mod, LPCSTR proc_name) { + resolve_kernel_functions(); + resolve_real_functions(); + + if (mod && proc_name && !is_ordinal_proc_name(proc_name) && is_ws2_family_module(mod)) { + if (const HookDef* hook = find_socket_hook(proc_name)) { + log_line( + "GetProcAddress(module=%s,proc=%s) => hook=%p", + module_name_for_handle(mod).c_str(), + proc_name, + hook->replacement); + return reinterpret_cast(hook->replacement); + } + } + + return g_real_getprocaddress ? g_real_getprocaddress(mod, proc_name) : nullptr; +} + +static SOCKET WSAAPI hook_socket(int af, int type, int protocol) { + resolve_real_functions(); + int requested_af = af; + bool forced = false; + + if (g_dualstack_enabled && af == AF_INET) { + af = AF_INET6; + forced = true; + } + + SOCKET s = g_real_socket ? g_real_socket(af, type, protocol) : INVALID_SOCKET; + int err = (s == INVALID_SOCKET) ? WSAGetLastError() : 0; + + if (s != INVALID_SOCKET) { + remember_socket(s, af, forced); + if (forced) { + int off = 0; + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&off, sizeof(off)); + } + } + + log_line( + "socket(req_af=%s/%d,real_af=%s/%d,type=%d,proto=%d) => sock=%llu err=%d dualstack=%d", + family_name(requested_af), + requested_af, + family_name(af), + af, + type, + protocol, + (unsigned long long)s, + err, + forced ? 1 : 0); + + if (s == INVALID_SOCKET) { + WSASetLastError(err); + } + return s; +} + +static SOCKET WSAAPI hook_wsasocketw( + int af, + int type, + int protocol, + LPWSAPROTOCOL_INFOW info, + GROUP group, + DWORD flags) { + resolve_real_functions(); + int requested_af = af; + bool forced = false; + + if (g_dualstack_enabled && af == AF_INET) { + af = AF_INET6; + forced = true; + } + + SOCKET s = + g_real_wsasocketw ? g_real_wsasocketw(af, type, protocol, info, group, flags) : INVALID_SOCKET; + int err = (s == INVALID_SOCKET) ? WSAGetLastError() : 0; + + if (s != INVALID_SOCKET) { + remember_socket(s, af, forced); + if (forced) { + int off = 0; + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&off, sizeof(off)); + } + } + + log_line( + "WSASocketW(req_af=%s/%d,real_af=%s/%d,type=%d,proto=%d,flags=0x%lx) => sock=%llu err=%d dualstack=%d", + family_name(requested_af), + requested_af, + family_name(af), + af, + type, + protocol, + (unsigned long)flags, + (unsigned long long)s, + err, + forced ? 1 : 0); + + if (s == INVALID_SOCKET) { + WSASetLastError(err); + } + return s; +} + +static SOCKET WSAAPI hook_wsasocketa( + int af, + int type, + int protocol, + LPWSAPROTOCOL_INFOA info, + GROUP group, + DWORD flags) { + resolve_real_functions(); + int requested_af = af; + bool forced = false; + + if (g_dualstack_enabled && af == AF_INET) { + af = AF_INET6; + forced = true; + } + + SOCKET s = + g_real_wsasocketa ? g_real_wsasocketa(af, type, protocol, info, group, flags) : INVALID_SOCKET; + int err = (s == INVALID_SOCKET) ? WSAGetLastError() : 0; + + if (s != INVALID_SOCKET) { + remember_socket(s, af, forced); + if (forced) { + int off = 0; + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&off, sizeof(off)); + } + } + + log_line( + "WSASocketA(req_af=%s/%d,real_af=%s/%d,type=%d,proto=%d,flags=0x%lx) => sock=%llu err=%d dualstack=%d", + family_name(requested_af), + requested_af, + family_name(af), + af, + type, + protocol, + (unsigned long)flags, + (unsigned long long)s, + err, + forced ? 1 : 0); + + if (s == INVALID_SOCKET) { + WSASetLastError(err); + } + return s; +} + +static int WSAAPI hook_closesocket(SOCKET s) { + resolve_real_functions(); + int rc = g_real_closesocket ? g_real_closesocket(s) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + forget_socket(s); + log_line("closesocket(sock=%llu) => rc=%d err=%d", (unsigned long long)s, rc, err); + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_connect(SOCKET s, const sockaddr* name, int namelen) { + resolve_real_functions(); + + const sockaddr* real_name = name; + int real_len = namelen; + sockaddr_in6 mapped{}; + bool translated = false; + bool forced_remote = false; + + if (should_translate_to_v6(s)) { + if (g_force_remote_enabled) { + real_name = reinterpret_cast(&g_forced_remote_addr); + real_len = (int)sizeof(g_forced_remote_addr); + translated = true; + forced_remote = true; + } else if (name && name->sa_family == AF_INET) { + mapped = v4_to_mapped(*reinterpret_cast(name)); + real_name = reinterpret_cast(&mapped); + real_len = (int)sizeof(mapped); + translated = true; + } + } + + int rc = g_real_connect ? g_real_connect(s, real_name, real_len) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + log_line( + "connect(sock=%llu,target=%s,translated=%d,forced_remote=%d) => rc=%d err=%d", + (unsigned long long)s, + format_sockaddr(name, namelen).c_str(), + translated ? 1 : 0, + forced_remote ? 1 : 0, + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_wsaconnect( + SOCKET s, + const sockaddr* name, + int namelen, + LPWSABUF callerData, + LPWSABUF calleeData, + LPQOS sqos, + LPQOS gqos) { + resolve_real_functions(); + + const sockaddr* real_name = name; + int real_len = namelen; + sockaddr_in6 mapped{}; + bool translated = false; + bool forced_remote = false; + + if (should_translate_to_v6(s)) { + if (g_force_remote_enabled) { + real_name = reinterpret_cast(&g_forced_remote_addr); + real_len = (int)sizeof(g_forced_remote_addr); + translated = true; + forced_remote = true; + } else if (name && name->sa_family == AF_INET) { + mapped = v4_to_mapped(*reinterpret_cast(name)); + real_name = reinterpret_cast(&mapped); + real_len = (int)sizeof(mapped); + translated = true; + } + } + + int rc = g_real_wsaconnect + ? g_real_wsaconnect(s, real_name, real_len, callerData, calleeData, sqos, gqos) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + log_line( + "WSAConnect(sock=%llu,target=%s,translated=%d,forced_remote=%d) => rc=%d err=%d", + (unsigned long long)s, + format_sockaddr(name, namelen).c_str(), + translated ? 1 : 0, + forced_remote ? 1 : 0, + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_bind(SOCKET s, const sockaddr* name, int namelen) { + resolve_real_functions(); + + const sockaddr* real_name = name; + int real_len = namelen; + sockaddr_in6 translated_addr{}; + bool translated = false; + + if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { + translated_addr = v4_bind_to_v6(*reinterpret_cast(name)); + real_name = reinterpret_cast(&translated_addr); + real_len = (int)sizeof(translated_addr); + translated = true; + } + + int rc = g_real_bind ? g_real_bind(s, real_name, real_len) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + log_line( + "bind(sock=%llu,addr=%s,translated=%d) => rc=%d err=%d", + (unsigned long long)s, + format_sockaddr(name, namelen).c_str(), + translated ? 1 : 0, + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_sendto( + SOCKET s, + const char* buf, + int len, + int flags, + const sockaddr* to, + int tolen) { + resolve_real_functions(); + + const sockaddr* real_to = to; + int real_tolen = tolen; + sockaddr_in6 mapped{}; + bool translated = false; + bool forced_remote = false; + + if (should_translate_to_v6(s)) { + if (g_force_remote_enabled) { + real_to = reinterpret_cast(&g_forced_remote_addr); + real_tolen = (int)sizeof(g_forced_remote_addr); + translated = true; + forced_remote = true; + } else if (to && to->sa_family == AF_INET) { + mapped = v4_to_mapped(*reinterpret_cast(to)); + real_to = reinterpret_cast(&mapped); + real_tolen = (int)sizeof(mapped); + translated = true; + } + } + + int rc = g_real_sendto ? g_real_sendto(s, buf, len, flags, real_to, real_tolen) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + log_line( + "sendto(sock=%llu,len=%d,to=%s,translated=%d,forced_remote=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + len, + format_sockaddr(to, tolen).c_str(), + translated ? 1 : 0, + forced_remote ? 1 : 0, + format_buffer_prefix(buf, static_cast(std::max(len, 0))).c_str(), + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_recv(SOCKET s, char* buf, int len, int flags) { + resolve_real_functions(); + + int rc = g_real_recv ? g_real_recv(s, buf, len, flags) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + log_line( + "recv(sock=%llu,len=%d,flags=0x%x,dualstack=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + len, + flags, + should_translate_to_v6(s) ? 1 : 0, + rc != SOCKET_ERROR ? format_buffer_prefix(buf, static_cast(rc)).c_str() : "-", + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_recvfrom( + SOCKET s, + char* buf, + int len, + int flags, + sockaddr* from, + int* fromlen) { + resolve_real_functions(); + + bool translate_back = should_translate_to_v6(s) && from && fromlen && *fromlen > 0; + if (!translate_back) { + int rc = g_real_recvfrom ? g_real_recvfrom(s, buf, len, flags, from, fromlen) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + log_line( + "recvfrom(sock=%llu,len=%d,from=%s,data=%s) => rc=%d err=%d", + (unsigned long long)s, + len, + from && fromlen ? format_sockaddr(from, *fromlen).c_str() : "(null)", + rc != SOCKET_ERROR ? format_buffer_prefix(buf, static_cast(rc)).c_str() : "-", + rc, + err); + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; + } + + sockaddr_storage tmp{}; + int tmp_len = sizeof(tmp); + int rc = g_real_recvfrom + ? g_real_recvfrom(s, buf, len, flags, reinterpret_cast(&tmp), &tmp_len) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + if (rc != SOCKET_ERROR) { + copy_or_translate_addr(tmp, tmp_len, from, fromlen, true); + } + + log_line( + "recvfrom(sock=%llu,len=%d,raw_from=%s,translated=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + len, + format_sockaddr(reinterpret_cast(&tmp), tmp_len).c_str(), + 1, + rc != SOCKET_ERROR ? format_buffer_prefix(buf, static_cast(rc)).c_str() : "-", + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_wsarecvex(SOCKET s, char* buf, int len, int* flags) { + resolve_real_functions(); + + int flags_before = flags ? *flags : 0; + int rc = g_real_wsarecvex ? g_real_wsarecvex(s, buf, len, flags) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + int flags_after = flags ? *flags : flags_before; + + log_line( + "WSARecvEx(sock=%llu,len=%d,flags_before=0x%x,flags_after=0x%x,dualstack=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + len, + flags_before, + flags_after, + should_translate_to_v6(s) ? 1 : 0, + rc != SOCKET_ERROR ? format_buffer_prefix(buf, static_cast(rc)).c_str() : "-", + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_wsarecv( + SOCKET s, + LPWSABUF buffers, + DWORD buffer_count, + LPDWORD bytes_received, + LPDWORD flags, + LPWSAOVERLAPPED overlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE completion_routine) { + resolve_real_functions(); + + int rc = g_real_wsarecv + ? g_real_wsarecv(s, buffers, buffer_count, bytes_received, flags, overlapped, completion_routine) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + DWORD received_value = bytes_received ? *bytes_received : 0; + DWORD flags_value = flags ? *flags : 0; + + log_line( + "WSARecv(sock=%llu,buffers=%lu,bytes=%lu,flags=0x%lx,overlapped=%d,dualstack=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + (unsigned long)buffer_count, + (unsigned long)received_value, + (unsigned long)flags_value, + overlapped ? 1 : 0, + should_translate_to_v6(s) ? 1 : 0, + (rc != SOCKET_ERROR && received_value > 0) + ? format_wsabuf_prefix(buffers, buffer_count, received_value).c_str() + : "-", + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_wsarecvfrom( + SOCKET s, + LPWSABUF buffers, + DWORD buffer_count, + LPDWORD bytes_received, + LPDWORD flags, + sockaddr* from, + LPINT fromlen, + LPWSAOVERLAPPED overlapped, + LPWSAOVERLAPPED_COMPLETION_ROUTINE completion_routine) { + resolve_real_functions(); + + const bool translate_back = should_translate_to_v6(s) && from && fromlen && *fromlen > 0; + if (!translate_back || overlapped || completion_routine) { + int rc = g_real_wsarecvfrom + ? g_real_wsarecvfrom( + s, + buffers, + buffer_count, + bytes_received, + flags, + from, + fromlen, + overlapped, + completion_routine) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + log_line( + "WSARecvFrom(sock=%llu,buffers=%lu,overlapped=%d,translated=%d,from=%s,data=%s) => rc=%d err=%d", + (unsigned long long)s, + (unsigned long)buffer_count, + overlapped ? 1 : 0, + 0, + from && fromlen ? format_sockaddr(from, *fromlen).c_str() : "(null)", + (rc != SOCKET_ERROR && bytes_received && *bytes_received > 0) + ? format_wsabuf_prefix(buffers, buffer_count, *bytes_received).c_str() + : "-", + rc, + err); + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; + } + + sockaddr_storage tmp{}; + int tmp_len = sizeof(tmp); + int rc = g_real_wsarecvfrom + ? g_real_wsarecvfrom( + s, + buffers, + buffer_count, + bytes_received, + flags, + reinterpret_cast(&tmp), + &tmp_len, + overlapped, + completion_routine) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + + if (rc != SOCKET_ERROR) { + copy_or_translate_addr(tmp, tmp_len, from, fromlen, true); + } + + DWORD received_value = bytes_received ? *bytes_received : 0; + DWORD flags_value = flags ? *flags : 0; + log_line( + "WSARecvFrom(sock=%llu,buffers=%lu,bytes=%lu,flags=0x%lx,raw_from=%s,translated=%d,data=%s) => rc=%d err=%d", + (unsigned long long)s, + (unsigned long)buffer_count, + (unsigned long)received_value, + (unsigned long)flags_value, + format_sockaddr(reinterpret_cast(&tmp), tmp_len).c_str(), + 1, + (rc != SOCKET_ERROR && received_value > 0) + ? format_wsabuf_prefix(buffers, buffer_count, received_value).c_str() + : "-", + rc, + err); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_getsockname(SOCKET s, sockaddr* name, int* namelen) { + resolve_real_functions(); + bool translate_back = should_translate_to_v6(s) && name && namelen && *namelen > 0; + + if (!translate_back) { + int rc = g_real_getsockname ? g_real_getsockname(s, name, namelen) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + log_line( + "getsockname(sock=%llu) => rc=%d err=%d addr=%s", + (unsigned long long)s, + rc, + err, + name && namelen ? format_sockaddr(name, *namelen).c_str() : "(null)"); + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; + } + + sockaddr_storage tmp{}; + int tmp_len = sizeof(tmp); + int rc = g_real_getsockname + ? g_real_getsockname(s, reinterpret_cast(&tmp), &tmp_len) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + if (rc != SOCKET_ERROR) { + copy_or_translate_addr(tmp, tmp_len, name, namelen, false); + } + + log_line( + "getsockname(sock=%llu) => rc=%d err=%d raw_addr=%s", + (unsigned long long)s, + rc, + err, + format_sockaddr(reinterpret_cast(&tmp), tmp_len).c_str()); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen) { + resolve_real_functions(); + bool translate_back = should_translate_to_v6(s) && name && namelen && *namelen > 0; + + if (!translate_back) { + int rc = g_real_getpeername ? g_real_getpeername(s, name, namelen) : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + log_line( + "getpeername(sock=%llu) => rc=%d err=%d addr=%s", + (unsigned long long)s, + rc, + err, + name && namelen ? format_sockaddr(name, *namelen).c_str() : "(null)"); + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; + } + + sockaddr_storage tmp{}; + int tmp_len = sizeof(tmp); + int rc = g_real_getpeername + ? g_real_getpeername(s, reinterpret_cast(&tmp), &tmp_len) + : SOCKET_ERROR; + int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; + if (rc != SOCKET_ERROR) { + copy_or_translate_addr(tmp, tmp_len, name, namelen, true); + } + + log_line( + "getpeername(sock=%llu) => rc=%d err=%d raw_addr=%s", + (unsigned long long)s, + rc, + err, + format_sockaddr(reinterpret_cast(&tmp), tmp_len).c_str()); + + if (rc == SOCKET_ERROR) { + WSASetLastError(err); + } + return rc; +} + +static DWORD WINAPI init_worker(void*) { + resolve_kernel_functions(); + resolve_real_functions(); + g_dualstack_enabled = get_env_bool("OMP_TRACE_DUALSTACK"); + g_force_remote_enabled = false; + std::memset(&g_forced_remote_addr, 0, sizeof(g_forced_remote_addr)); + + std::string remote_ipv6 = normalize_ipv6(get_env_string("OMP_TRACE_REMOTE_IPV6")); + std::string remote_port = get_env_string("OMP_TRACE_REMOTE_PORT"); + unsigned short parsed_port = 0; + if (!remote_ipv6.empty() && parse_u16(remote_port, &parsed_port)) { + sockaddr_in6 forced{}; + forced.sin6_family = AF_INET6; + forced.sin6_port = htons(parsed_port); + if (InetPtonA(AF_INET6, remote_ipv6.c_str(), &forced.sin6_addr) == 1) { + g_forced_remote_addr = forced; + g_force_remote_enabled = true; + } + } + + init_log_path(); + + std::string forced_target = g_force_remote_enabled + ? format_sockaddr( + reinterpret_cast(&g_forced_remote_addr), + (int)sizeof(g_forced_remote_addr)) + : "(disabled)"; + + log_line( + "omp-socket-trace loaded; dualstack=%d; forced_remote=%d; remote_target=%s; log=%s", + g_dualstack_enabled ? 1 : 0, + g_force_remote_enabled ? 1 : 0, + forced_target.c_str(), + g_log_path.c_str()); + + DWORD loop = 0; + for (;;) { + patch_loaded_modules(); + g_initialized.store(true, std::memory_order_release); + + ++loop; + if (loop < 60) { + Sleep(250); + } else { + Sleep(2000); + } + } +} + +} // namespace + +BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) { + if (reason == DLL_PROCESS_ATTACH) { + g_self_module = module; + DisableThreadLibraryCalls(module); + HANDLE thread = CreateThread(nullptr, 0, init_worker, nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } + } + return TRUE; +}