From 4a01b7b9433b6d1615426e60f111b359b894eb7e Mon Sep 17 00:00:00 2001 From: sampmoder Date: Sat, 14 Feb 2026 14:02:39 +0900 Subject: [PATCH 01/30] Create ko.ts I have added the 'ko.json' file with complete translations to improve accessibility for Korean players. Changes: - Added assets/locales/ko.json - Registered 'ko' in the language selection list Please review and merge this when you have time. Thanks! --- src/locales/translations/ko.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/locales/translations/ko.ts diff --git a/src/locales/translations/ko.ts b/src/locales/translations/ko.ts new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/locales/translations/ko.ts @@ -0,0 +1 @@ + From a1c625542f4161fe870aa83436f76879f776cf92 Mon Sep 17 00:00:00 2001 From: Prince_Jess Date: Sat, 14 Feb 2026 15:19:41 +0900 Subject: [PATCH 02/30] Create kr.ts --- src/locales/translations/kr.ts | 124 +++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/locales/translations/kr.ts 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 디렉토리에 없습니다. 설정 -> 고급 탭에서 확인해 주세요.", +}; From 7180c7972eff3ed0570cb695dde6a321f4e016e6 Mon Sep 17 00:00:00 2001 From: Prince_Jess Date: Sat, 14 Feb 2026 15:24:15 +0900 Subject: [PATCH 03/30] Update index.ts --- src/locales/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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; + From 76fef91466d8bd2c5dbb9d81dc4a36337ba0e762 Mon Sep 17 00:00:00 2001 From: Prince_Jess Date: Sat, 14 Feb 2026 15:28:50 +0900 Subject: [PATCH 04/30] Delete src/locales/translations/ko.ts --- src/locales/translations/ko.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/locales/translations/ko.ts diff --git a/src/locales/translations/ko.ts b/src/locales/translations/ko.ts deleted file mode 100644 index 8b137891..00000000 --- a/src/locales/translations/ko.ts +++ /dev/null @@ -1 +0,0 @@ - From c03565561bacf8b7bdd925bd2327eddef53fd75e Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:08:52 +0100 Subject: [PATCH 05/30] Add IPv6-aware launcher query backend --- src-tauri/src/commands.rs | 13 ++--- src-tauri/src/constants.rs | 1 + src-tauri/src/query.rs | 114 +++++++++++++++++-------------------- 3 files changed, 57 insertions(+), 71 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 19369ceb..dcc00dc9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -80,7 +80,7 @@ pub fn get_samp_favorite_list() -> String { #[tauri::command] pub fn resolve_hostname(hostname: String) -> std::result::Result { - use std::net::{IpAddr, ToSocketAddrs}; + use std::net::ToSocketAddrs; if hostname.is_empty() { return Err("Hostname cannot be empty".to_string()); @@ -91,13 +91,10 @@ pub fn resolve_hostname(hostname: String) -> std::result::Result .to_socket_addrs() .map_err(|e| format!("Failed to resolve hostname '{}': {}", hostname, e))?; - for ip in addrs { - if let IpAddr::V4(ipv4) = ip.ip() { - return Ok(ipv4.to_string()); - } - } - - Err(format!("No IPv4 address found for hostname '{}'", hostname)) + addrs + .map(|socket_addr| socket_addr.ip().to_string()) + .next() + .ok_or_else(|| format!("No address found for hostname '{}'", hostname)) } #[tauri::command] diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 088479f8..1ab001bd 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -24,6 +24,7 @@ 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/query.rs b/src-tauri/src/query.rs index 72e5c839..5a11e124 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::{SystemTime, UNIX_EPOCH}; -use std::{net::Ipv4Addr, time::Duration}; +use std::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,60 @@ 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 +170,15 @@ 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), From a066fcfc9840856d0b63898e6977698d3ef579a1 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:09:48 +0100 Subject: [PATCH 06/30] Add IPv6 endpoint parsing helpers --- src/utils/helpers.ts | 7 ++-- src/utils/types.ts | 18 ++++++---- src/utils/validation.ts | 74 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 75a440ab..23f674b8 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -17,7 +17,7 @@ import { Server, SortType, } from "./types"; -import { validateServerAddressIPv4 } from "./validation"; +import { parseServerAddress, validateServerAddressIPv4 } from "./validation"; // Server update configuration const SERVER_UPDATE_CONFIG = { @@ -38,8 +38,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, 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; }; From 84fbaa8dcdc60e383482e6ee25b77fd8e7a0dc2c Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:10:23 +0100 Subject: [PATCH 07/30] Use IPv6 endpoint parsing in launcher dialogs --- src/containers/AddThirdPartyServer/index.tsx | 31 +++-------- .../ExternalServerHandler/index.tsx | 51 +++++-------------- 2 files changed, 20 insertions(+), 62 deletions(-) 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); } From f4e7688073b924298d2f84d365af3010abdbbfa0 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:11:01 +0100 Subject: [PATCH 08/30] Handle IPv6 addresses in launcher join flow --- src/utils/game.ts | 14 +++++++++----- src/utils/helpers.ts | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/utils/game.ts b/src/utils/game.ts index a0bb3442..77cc223e 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, normalizeIPv6 } from "./validation"; const showOkModal = (title: string, description: string) => { const { showMessageBox, hideMessageBox } = useMessageBox.getState(); @@ -72,15 +73,18 @@ 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; + const connectAddress = + resolvedAddress && isIPv6(resolvedAddress) + ? `[${normalizeIPv6(resolvedAddress)}]` + : resolvedAddress; 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}:${server.port}:${nickname}:${password}` + : `connect:${connectAddress}:${server.port}:${nickname}`, }); return; } @@ -221,7 +225,7 @@ export const startGame = async ( invoke("inject", { name: nickname, - ip: await getIpAddress(server.ip), + ip: resolvedAddress, port: server.port, exe: gtasaPath, dll: ourSAMPDllPath, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 23f674b8..d7e029ca 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -17,7 +17,7 @@ import { Server, SortType, } from "./types"; -import { parseServerAddress, validateServerAddressIPv4 } from "./validation"; +import { parseServerAddress, validateServerAddress } from "./validation"; // Server update configuration const SERVER_UPDATE_CONFIG = { @@ -198,7 +198,7 @@ export const getIpAddress = async ( } // Use validation function from validation.ts - if (validateServerAddressIPv4(hostname)) { + if (validateServerAddress(hostname)) { return hostname; } From fe8407c5e1e25d0fcb66f304475230a6b29658fe Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:11:18 +0100 Subject: [PATCH 09/30] Add launcher query probe skeleton --- src-tauri/src/bin/omp_query_probe.rs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src-tauri/src/bin/omp_query_probe.rs 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..07d905b2 --- /dev/null +++ b/src-tauri/src/bin/omp_query_probe.rs @@ -0,0 +1,6 @@ +fn main() { + eprintln!("Usage: omp_query_probe [opcode]"); + eprintln!(" family: ipv4 | ipv6"); + eprintln!(" opcode: i | o | c | r | p"); + std::process::exit(1); +} From 8f9e1bd03b5653f1ca8058e718d6a4a6faab2642 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:11:48 +0100 Subject: [PATCH 10/30] Implement launcher query probe --- src-tauri/src/bin/omp_query_probe.rs | 132 ++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/bin/omp_query_probe.rs b/src-tauri/src/bin/omp_query_probe.rs index 07d905b2..e458bebe 100644 --- a/src-tauri/src/bin/omp_query_probe.rs +++ b/src-tauri/src/bin/omp_query_probe.rs @@ -1,6 +1,134 @@ -fn main() { +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"); - std::process::exit(1); +} + +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!(); } From 8b8bfa5b3183b0482c8b2977947e017dd26cd137 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:21:07 +0100 Subject: [PATCH 11/30] Add launcher connect probe skeleton --- src-tauri/src/bin/omp_connect_probe.rs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src-tauri/src/bin/omp_connect_probe.rs 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..133a5664 --- /dev/null +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -0,0 +1,5 @@ +fn main() { + eprintln!("Usage: omp_connect_probe "); + eprintln!(" family: ipv4 | ipv6"); + std::process::exit(1); +} From 38b69165c8c8b0814cb567cc73dcee07c9489b8e Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:21:40 +0100 Subject: [PATCH 12/30] Implement launcher connect probe --- src-tauri/src/bin/omp_connect_probe.rs | 120 ++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/bin/omp_connect_probe.rs b/src-tauri/src/bin/omp_connect_probe.rs index 133a5664..41eb9457 100644 --- a/src-tauri/src/bin/omp_connect_probe.rs +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -1,5 +1,121 @@ -fn main() { +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_CONNECTION_ATTEMPT_FAILED: u8 = 29; +const ID_NO_FREE_INCOMING_CONNECTIONS: u8 = 31; + +fn print_usage() { eprintln!("Usage: omp_connect_probe "); eprintln!(" family: ipv4 | ipv6"); - std::process::exit(1); +} + +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 request = [ID_OPEN_CONNECTION_REQUEST, 0, 0]; + if let Err(e) = socket.send_to(&request, target) { + eprintln!("send_to failed: {e}"); + std::process::exit(1); + } + + let mut response = [0_u8; 128]; + let received = match socket.recv(&mut response) { + Ok(n) => n, + Err(e) => { + eprintln!("recv failed: {e}"); + std::process::exit(1); + } + }; + + let 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!(); } From 9ca779b0213087cbdc543e1f42978f401ae590a6 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:24:07 +0100 Subject: [PATCH 13/30] Handle cookie challenge in launcher connect probe --- src-tauri/src/bin/omp_connect_probe.rs | 42 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/bin/omp_connect_probe.rs b/src-tauri/src/bin/omp_connect_probe.rs index 41eb9457..f05176ea 100644 --- a/src-tauri/src/bin/omp_connect_probe.rs +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -4,8 +4,10 @@ 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 "); @@ -76,14 +78,22 @@ fn main() { }; socket.set_read_timeout(Some(Duration::from_secs(2))).ok(); - let request = [ID_OPEN_CONNECTION_REQUEST, 0, 0]; - if let Err(e) = socket.send_to(&request, target) { + let mut response = [0_u8; 128]; + let mut 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 response = [0_u8; 128]; - let received = match socket.recv(&mut response) { + let mut received = match socket.recv(&mut response) { Ok(n) => n, Err(e) => { eprintln!("recv failed: {e}"); @@ -91,7 +101,29 @@ fn main() { } }; - let packet_id = response[0]; + 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); From e925de170f20c7109221abd94b46c82bbdcfb5fb Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 2 Mar 2026 17:27:26 +0100 Subject: [PATCH 14/30] Clean launcher connect probe warning --- src-tauri/src/bin/omp_connect_probe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/bin/omp_connect_probe.rs b/src-tauri/src/bin/omp_connect_probe.rs index f05176ea..c0c26f4c 100644 --- a/src-tauri/src/bin/omp_connect_probe.rs +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -79,7 +79,7 @@ fn main() { socket.set_read_timeout(Some(Duration::from_secs(2))).ok(); let mut response = [0_u8; 128]; - let mut send_request = |cookie_xor: u16| { + let send_request = |cookie_xor: u16| { let request = [ ID_OPEN_CONNECTION_REQUEST, (cookie_xor & 0xFF) as u8, From 590280551332f8c170eaa03abefeb47822012457 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 20:10:01 +0100 Subject: [PATCH 15/30] launcher: add socket trace injector and ipv6 probe tooling --- src-tauri/Cargo.lock | 388 +++++++---- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 2 +- src-tauri/src/bin/omp_connect_probe.rs | 14 +- src-tauri/src/bin/omp_query_probe.rs | 19 +- src-tauri/src/commands.rs | 2 + src-tauri/src/injector.rs | 10 + src-tauri/src/main.rs | 7 +- src-tauri/src/query.rs | 28 +- src/utils/game.ts | 3 + tools/win-socket-trace/CMakeLists.txt | 11 + tools/win-socket-trace/README.md | 57 ++ tools/win-socket-trace/wsock_trace.cpp | 923 +++++++++++++++++++++++++ 13 files changed, 1319 insertions(+), 147 deletions(-) create mode 100644 tools/win-socket-trace/CMakeLists.txt create mode 100644 tools/win-socket-trace/README.md create mode 100644 tools/win-socket-trace/wsock_trace.cpp 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 index c0c26f4c..7343ac0c 100644 --- a/src-tauri/src/bin/omp_connect_probe.rs +++ b/src-tauri/src/bin/omp_connect_probe.rs @@ -23,7 +23,11 @@ fn parse_family(input: &str) -> Option { } fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result { - if let Ok(ip) = host.trim_start_matches('[').trim_end_matches(']').parse::() { + if let Ok(ip) = host + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + { return Ok(SocketAddr::new(ip, port)); } @@ -33,7 +37,13 @@ fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result Option { } fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result { - if let Ok(ip) = host.trim_start_matches('[').trim_end_matches(']').parse::() { + if let Ok(ip) = host + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + { return Ok(SocketAddr::new(ip, port)); } @@ -30,7 +34,13 @@ fn resolve_target(host: &str, port: u16, want_ipv6: bool) -> Result Vec { @@ -80,7 +90,10 @@ fn main() { std::process::exit(1); } }; - let opcode = args.get(4).and_then(|value| value.as_bytes().first().copied()).unwrap_or(b'i'); + 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, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index dcc00dc9..23f62bd8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -12,6 +12,7 @@ pub async fn inject( port: i32, exe: &str, dll: &str, + trace_file: &str, omp_file: &str, password: &str, custom_game_exe: &str, @@ -28,6 +29,7 @@ pub async fn inject( port, exe, dll, + trace_file, actual_omp_file, password, custom_game_exe, diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 98c59287..674d7adc 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -17,6 +17,7 @@ pub async fn run_samp( _port: i32, _executable_dir: &str, _dll_path: &str, + _trace_file: &str, _omp_file: &str, _password: &str, _custom_game_exe: &str, @@ -31,6 +32,7 @@ pub async fn run_samp( port: i32, executable_dir: &str, dll_path: &str, + trace_file: &str, omp_file: &str, password: &str, custom_game_exe: &str, @@ -71,6 +73,14 @@ pub async fn run_samp( match process { Ok(p) => { + if !trace_file.is_empty() { + if let Err(e) = inject_dll(p.id(), trace_file, 0, false) { + info!( + "[run_samp] optional trace DLL injection failed for {}: {}", + trace_file, e + ); + } + } inject_dll(p.id(), dll_path, 0, false)?; info!("[run_samp] omp_file.is_empty(): {}", omp_file.is_empty()); if !omp_file.is_empty() { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a0c29f60..543fe506 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -124,11 +124,7 @@ 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(), @@ -136,6 +132,7 @@ async fn handle_cli_args() -> Result<()> { args.port.unwrap(), gamepath, &format!("{}/{}", gamepath, SAMP_DLL), + "", omp_path, &password, "", diff --git a/src-tauri/src/query.rs b/src-tauri/src/query.rs index 5a11e124..fd5d07f5 100644 --- a/src-tauri/src/query.rs +++ b/src-tauri/src/query.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; use std::io::{Cursor, Read}; use std::net::{IpAddr, SocketAddr}; use std::sync::Mutex; -use std::time::{SystemTime, UNIX_EPOCH}; use std::time::Duration; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::net::{lookup_host, UdpSocket}; use tokio::time::timeout_at; use tokio::time::Instant; @@ -87,19 +87,21 @@ impl Query { let target = if normalized_addr.parse::().is_ok() { SocketAddr::new( - normalized_addr - .parse::() - .map_err(|e| LauncherError::InvalidInput(format!("Invalid IP address: {}", e)))?, + 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 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 bind_addr = match target { @@ -175,7 +177,9 @@ impl Query { } 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())); + return Err(LauncherError::Network( + "Unknown query response format".to_string(), + )); }; let packet = Cursor::new(buf[payload_offset..amt].to_vec()); diff --git a/src/utils/game.ts b/src/utils/game.ts index 77cc223e..7b15afa0 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -222,6 +222,8 @@ export const startGame = async ( : file ? await getLocalPath(file.path, file.name) : idealSAMPDllPath; + const traceDllPath = await getLocalPath("omp", "omp-socket-trace.dll"); + const traceFile = (await fs.exists(traceDllPath)) ? traceDllPath : ""; invoke("inject", { name: nickname, @@ -229,6 +231,7 @@ export const startGame = async ( port: server.port, exe: gtasaPath, dll: ourSAMPDllPath, + traceFile, ompFile: await getLocalPath("omp", "omp-client.dll"), password, customGameExe, diff --git a/tools/win-socket-trace/CMakeLists.txt b/tools/win-socket-trace/CMakeLists.txt new file mode 100644 index 00000000..b80d9ff9 --- /dev/null +++ b/tools/win-socket-trace/CMakeLists.txt @@ -0,0 +1,11 @@ +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) diff --git a/tools/win-socket-trace/README.md b/tools/win-socket-trace/README.md new file mode 100644 index 00000000..d5be2bfd --- /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 -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..353ea956 --- /dev/null +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -0,0 +1,923 @@ +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif + +#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 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 recvfrom_fn_t = int(WSAAPI*)(SOCKET, char*, int, int, sockaddr*, int*); +using getsockname_fn_t = int(WSAAPI*)(SOCKET, sockaddr*, int*); +using getpeername_fn_t = int(WSAAPI*)(SOCKET, sockaddr*, int*); + +static socket_fn_t g_real_socket = nullptr; +static wsasocketa_fn_t g_real_wsasocketa = 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 recvfrom_fn_t g_real_recvfrom = nullptr; +static getsockname_fn_t g_real_getsockname = nullptr; +static getpeername_fn_t g_real_getpeername = 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::atomic g_initialized{false}; +static bool g_dualstack_enabled = false; + +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 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_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 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 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 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) { + 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{}; + if (mapped_to_v4(*reinterpret_cast(&src), &out4)) { + 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) { + fn = reinterpret_cast(GetProcAddress(ws2, name)); + } +} + +static void resolve_real_functions() { + HMODULE ws2 = GetModuleHandleA("ws2_32.dll"); + if (!ws2) { + ws2 = LoadLibraryA("ws2_32.dll"); + } + resolve_real(g_real_socket, ws2, "socket"); + resolve_real(g_real_wsasocketa, ws2, "WSASocketA"); + 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_recvfrom, ws2, "recvfrom"); + resolve_real(g_real_getsockname, ws2, "getsockname"); + resolve_real(g_real_getpeername, ws2, "getpeername"); +} + +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; + } + + auto* thunk = reinterpret_cast(base + imp->FirstThunk); + auto* orig = reinterpret_cast( + base + (imp->OriginalFirstThunk ? imp->OriginalFirstThunk : imp->FirstThunk)); + + for (; orig->u1.AddressOfData; ++orig, ++thunk) { + if (IMAGE_SNAP_BY_ORDINAL(orig->u1.Ordinal)) { + continue; + } + + auto* by_name = reinterpret_cast(base + orig->u1.AddressOfData); + if (strcmp(reinterpret_cast(by_name->Name), func_name) != 0) { + continue; + } + + auto current = reinterpret_cast((uintptr_t)thunk->u1.Function); + 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; + } + } + + 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 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_getsockname(SOCKET s, sockaddr* name, int* namelen); +static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen); + +static HookDef kHooks[] = { + {"socket", (void*)&hook_socket, (void**)&g_real_socket}, + {"WSASocketA", (void*)&hook_wsasocketa, (void**)&g_real_wsasocketa}, + {"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}, + {"recvfrom", (void*)&hook_recvfrom, (void**)&g_real_recvfrom}, + {"getsockname", (void*)&hook_getsockname, (void**)&g_real_getsockname}, + {"getpeername", (void*)&hook_getpeername, (void**)&g_real_getpeername}, +}; + +static void apply_hooks_for_module(HMODULE mod) { + for (const auto& hook : kHooks) { + 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); + } +} + +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 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_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; + + if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { + 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) => 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_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; + + if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { + 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) => 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_bind(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; + + if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { + mapped = v4_to_mapped(*reinterpret_cast(name)); + real_name = reinterpret_cast(&mapped); + real_len = (int)sizeof(mapped); + 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; + + if (to && to->sa_family == AF_INET && should_translate_to_v6(s)) { + 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) => rc=%d err=%d", + (unsigned long long)s, + len, + format_sockaddr(to, tolen).c_str(), + translated ? 1 : 0, + 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) => rc=%d err=%d", + (unsigned long long)s, + len, + from && fromlen ? format_sockaddr(from, *fromlen).c_str() : "(null)", + 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); + } + + log_line( + "recvfrom(sock=%llu,len=%d,raw_from=%s,translated=%d) => rc=%d err=%d", + (unsigned long long)s, + len, + format_sockaddr(reinterpret_cast(&tmp), tmp_len).c_str(), + 1, + 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); + } + + 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); + } + + 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_real_functions(); + g_dualstack_enabled = get_env_bool("OMP_TRACE_DUALSTACK"); + init_log_path(); + + log_line( + "omp-socket-trace loaded; dualstack=%d; log=%s", + g_dualstack_enabled ? 1 : 0, + 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) { + DisableThreadLibraryCalls(module); + HANDLE thread = CreateThread(nullptr, 0, init_worker, nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } + } + return TRUE; +} From 7acd4862db0b6ef78701866ab3f05eefcf4411ff Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 20:25:43 +0100 Subject: [PATCH 16/30] launcher: add ipv6 probe fallback and dualstack trace flag --- src-tauri/src/commands.rs | 125 +++++++++++++++++++++++++++++++++++--- src-tauri/src/injector.rs | 9 +++ src-tauri/src/main.rs | 2 + src/utils/game.ts | 45 ++++++++++++-- src/utils/helpers.ts | 27 ++++++-- 5 files changed, 191 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 23f62bd8..8adcc90c 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( @@ -13,6 +17,7 @@ pub async fn inject( exe: &str, dll: &str, trace_file: &str, + trace_dualstack: Option, omp_file: &str, password: &str, custom_game_exe: &str, @@ -30,6 +35,7 @@ pub async fn inject( exe, dll, trace_file, + trace_dualstack.unwrap_or(false), actual_omp_file, password, custom_game_exe, @@ -81,22 +87,125 @@ pub fn get_samp_favorite_list() -> String { } #[tauri::command] -pub fn resolve_hostname(hostname: String) -> std::result::Result { +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)), + } +} + +#[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); + } + + 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), + }; - addrs - .map(|socket_addr| socket_addr.ip().to_string()) - .next() - .ok_or_else(|| format!("No address found for hostname '{}'", hostname)) + Ok(received >= 24 && &buf[..5] == SAMP6_PACKET_HEADER) } #[tauri::command] diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 674d7adc..04d5a56a 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -18,6 +18,7 @@ pub async fn run_samp( _executable_dir: &str, _dll_path: &str, _trace_file: &str, + _trace_dualstack: bool, _omp_file: &str, _password: &str, _custom_game_exe: &str, @@ -33,6 +34,7 @@ pub async fn run_samp( executable_dir: &str, dll_path: &str, trace_file: &str, + trace_dualstack: bool, omp_file: &str, password: &str, custom_game_exe: &str, @@ -69,6 +71,13 @@ 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" }, + ); + } + let process = ready_for_exec.current_dir(executable_dir).spawn(); match process { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 543fe506..575aa8b1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -133,6 +133,7 @@ async fn handle_cli_args() -> Result<()> { gamepath, &format!("{}/{}", gamepath, SAMP_DLL), "", + false, omp_path, &password, "", @@ -172,6 +173,7 @@ async fn run_tauri_app() -> Result<()> { commands::get_samp_favorite_list, commands::rerun_as_admin, commands::resolve_hostname, + commands::probe_ipv6_query, commands::is_process_alive, commands::log_info, commands::log_warn, diff --git a/src/utils/game.ts b/src/utils/game.ts index 7b15afa0..a5a36abc 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -74,10 +74,43 @@ export const startGame = async ( const { showPrompt, setServer } = useJoinServerPrompt.getState(); const { setSelected } = useServers.getState(); const resolvedAddress = (await getIpAddress(server.ip)) ?? server.ip; + let launchAddress = resolvedAddress; + let traceDualstack = false; + + if (resolvedAddress && isIPv6(resolvedAddress)) { + const normalizedIPv6 = normalizeIPv6(resolvedAddress); + let ipv6ProbeOk = false; + + try { + ipv6ProbeOk = await invoke("probe_ipv6_query", { + host: normalizedIPv6, + port: server.port, + }); + traceDualstack = ipv6ProbeOk; + } catch (error) { + Log.warn("[startGame] IPv6 probe failed unexpectedly:", error); + } + + if (!ipv6ProbeOk) { + const fallbackIPv4 = await getIpAddress(server.ip, "ipv4"); + if (fallbackIPv4 && !isIPv6(fallbackIPv4)) { + launchAddress = fallbackIPv4; + traceDualstack = false; + Log.warn( + `[startGame] IPv6 probe failed for ${normalizedIPv6}:${server.port}, falling back to IPv4 ${fallbackIPv4}` + ); + } else { + Log.warn( + `[startGame] IPv6 probe failed for ${normalizedIPv6}:${server.port} and no IPv4 fallback was found` + ); + } + } + } + const connectAddress = - resolvedAddress && isIPv6(resolvedAddress) - ? `[${normalizeIPv6(resolvedAddress)}]` - : resolvedAddress; + launchAddress && isIPv6(launchAddress) + ? `[${normalizeIPv6(launchAddress)}]` + : launchAddress; if (IN_GAME) { invoke("send_message_to_game", { @@ -224,14 +257,18 @@ export const startGame = async ( : idealSAMPDllPath; const traceDllPath = await getLocalPath("omp", "omp-socket-trace.dll"); const traceFile = (await fs.exists(traceDllPath)) ? traceDllPath : ""; + if (!traceFile.length) { + traceDualstack = false; + } invoke("inject", { name: nickname, - ip: resolvedAddress, + ip: launchAddress, port: server.port, exe: gtasaPath, dll: ourSAMPDllPath, traceFile, + traceDualstack, ompFile: await getLocalPath("omp", "omp-client.dll"), password, customGameExe, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index d7e029ca..3b9144d7 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -17,7 +17,12 @@ import { Server, SortType, } from "./types"; -import { parseServerAddress, validateServerAddress } from "./validation"; +import { + isIPv4, + isIPv6, + parseServerAddress, + validateServerAddress, +} from "./validation"; // Server update configuration const SERVER_UPDATE_CONFIG = { @@ -190,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 (validateServerAddress(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) { From 7ba37196b0e66e848268c649e8b5022c1147d75a Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:03:20 +0100 Subject: [PATCH 17/30] launcher: force ipv6 shim remote target and tighten trace fallback --- src-tauri/src/commands.rs | 4 + src-tauri/src/injector.rs | 13 ++ src-tauri/src/main.rs | 2 + src/utils/game.ts | 58 +++++++- tools/win-socket-trace/wsock_trace.cpp | 181 ++++++++++++++++++++++--- 5 files changed, 233 insertions(+), 25 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 8adcc90c..8b99e8aa 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,6 +18,8 @@ pub async fn inject( 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, @@ -36,6 +38,8 @@ pub async fn inject( 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, diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 04d5a56a..cd2dd935 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -19,6 +19,8 @@ pub async fn run_samp( _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, @@ -35,6 +37,8 @@ pub async fn run_samp( 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, @@ -76,6 +80,15 @@ pub async fn run_samp( "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()); + } } let process = ready_for_exec.current_dir(executable_dir).spawn(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 575aa8b1..88764131 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -134,6 +134,8 @@ async fn handle_cli_args() -> Result<()> { &format!("{}/{}", gamepath, SAMP_DLL), "", false, + "", + 0, omp_path, &password, "", diff --git a/src/utils/game.ts b/src/utils/game.ts index a5a36abc..18a085a8 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -76,6 +76,9 @@ export const startGame = async ( const resolvedAddress = (await getIpAddress(server.ip)) ?? server.ip; let launchAddress = resolvedAddress; let traceDualstack = false; + let traceRemoteIp = ""; + let traceRemotePort = 0; + let injectAddress = launchAddress; if (resolvedAddress && isIPv6(resolvedAddress)) { const normalizedIPv6 = normalizeIPv6(resolvedAddress); @@ -255,20 +258,69 @@ export const startGame = async ( : file ? await getLocalPath(file.path, file.name) : idealSAMPDllPath; - const traceDllPath = await getLocalPath("omp", "omp-socket-trace.dll"); - const traceFile = (await fs.exists(traceDllPath)) ? traceDllPath : ""; + const traceCandidates: string[] = [ + await getLocalPath("omp", "omp-socket-trace.dll"), + await path.join(gtasaPath, "omp-socket-trace.dll"), + ]; + + try { + const launcherDir = await path.executableDir(); + traceCandidates.push(await path.join(launcherDir, "omp-socket-trace.dll")); + } catch (error) { + Log.warn( + "[startGame] Failed to resolve launcher directory for trace DLL lookup:", + error + ); + } + + let traceFile = ""; + for (const candidate of traceCandidates) { + if (await fs.exists(candidate)) { + traceFile = candidate; + break; + } + } + if (!traceFile.length) { traceDualstack = false; + + if (launchAddress && isIPv6(launchAddress)) { + const fallbackIPv4 = await getIpAddress(server.ip, "ipv4"); + if (fallbackIPv4 && !isIPv6(fallbackIPv4)) { + Log.warn( + `[startGame] IPv6 launch requires omp-socket-trace.dll; falling back to IPv4 ${fallbackIPv4}` + ); + launchAddress = fallbackIPv4; + } else { + showOkModal( + "IPv6 compatibility layer missing", + "omp-socket-trace.dll was not found, and no IPv4 fallback address is available for this server." + ); + showPrompt(true); + setServer(server); + return; + } + } + } + + if (launchAddress && isIPv6(launchAddress) && traceDualstack && traceFile.length) { + traceRemoteIp = normalizeIPv6(launchAddress); + traceRemotePort = server.port; + injectAddress = "127.0.0.1"; + } else { + injectAddress = launchAddress; } invoke("inject", { name: nickname, - ip: launchAddress, + ip: injectAddress, port: server.port, exe: gtasaPath, dll: ourSAMPDllPath, traceFile, traceDualstack, + traceRemoteIp, + traceRemotePort, ompFile: await getLocalPath("omp", "omp-client.dll"), password, customGameExe, diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 353ea956..672ff1ce 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,7 @@ struct SocketMeta { 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 = @@ -37,6 +39,7 @@ using getpeername_fn_t = int(WSAAPI*)(SOCKET, sockaddr*, int*); 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; @@ -55,6 +58,8 @@ static std::unordered_map g_socket_meta; 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 const char* family_name(int af) { switch (af) { @@ -93,6 +98,31 @@ static bool get_env_bool(const char* key) { 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; @@ -331,6 +361,7 @@ static void resolve_real_functions() { } 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"); @@ -441,6 +472,13 @@ static SOCKET WSAAPI hook_wsasocketa( 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( @@ -472,6 +510,7 @@ static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen); static HookDef kHooks[] = { {"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}, @@ -546,6 +585,53 @@ static SOCKET WSAAPI hook_socket(int af, int type, int protocol) { 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, @@ -612,22 +698,31 @@ static int WSAAPI hook_connect(SOCKET s, const sockaddr* name, int namelen) { int real_len = namelen; sockaddr_in6 mapped{}; bool translated = false; - - if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { - mapped = v4_to_mapped(*reinterpret_cast(name)); - real_name = reinterpret_cast(&mapped); - real_len = (int)sizeof(mapped); - translated = true; + 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) => rc=%d err=%d", + "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); @@ -651,12 +746,20 @@ static int WSAAPI hook_wsaconnect( int real_len = namelen; sockaddr_in6 mapped{}; bool translated = false; - - if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { - mapped = v4_to_mapped(*reinterpret_cast(name)); - real_name = reinterpret_cast(&mapped); - real_len = (int)sizeof(mapped); - translated = true; + 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 @@ -665,10 +768,11 @@ static int WSAAPI hook_wsaconnect( int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; log_line( - "WSAConnect(sock=%llu,target=%s,translated=%d) => rc=%d err=%d", + "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); @@ -723,23 +827,32 @@ static int WSAAPI hook_sendto( int real_tolen = tolen; sockaddr_in6 mapped{}; bool translated = false; - - if (to && to->sa_family == AF_INET && should_translate_to_v6(s)) { - mapped = v4_to_mapped(*reinterpret_cast(to)); - real_to = reinterpret_cast(&mapped); - real_tolen = (int)sizeof(mapped); - translated = true; + 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) => rc=%d err=%d", + "sendto(sock=%llu,len=%d,to=%s,translated=%d,forced_remote=%d) => rc=%d err=%d", (unsigned long long)s, len, format_sockaddr(to, tolen).c_str(), translated ? 1 : 0, + forced_remote ? 1 : 0, rc, err); @@ -888,11 +1001,35 @@ static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen) { static DWORD WINAPI init_worker(void*) { 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; log=%s", + "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; From 679b9881eb0ccd7f934da7783205eead826e2941 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:28:23 +0100 Subject: [PATCH 18/30] launcher: stage trace runtime and fail fast on optional shim --- src-tauri/src/injector.rs | 7 +++++- src/utils/game.ts | 52 ++++++++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index cd2dd935..660a0171 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -10,6 +10,11 @@ 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(not(target_os = "windows"))] pub async fn run_samp( _name: &str, @@ -96,7 +101,7 @@ pub async fn run_samp( match process { Ok(p) => { if !trace_file.is_empty() { - if let Err(e) = inject_dll(p.id(), trace_file, 0, false) { + if let Err(e) = inject_optional_dll(p.id(), trace_file) { info!( "[run_samp] optional trace DLL injection failed for {}: {}", trace_file, e diff --git a/src/utils/game.ts b/src/utils/game.ts index 18a085a8..ccfce03f 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -1,6 +1,6 @@ import { fs, invoke, path, process, shell } from "@tauri-apps/api"; import { open, save } from "@tauri-apps/api/dialog"; -import { exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; +import { copyFile, exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; import { t } from "i18next"; import { IN_GAME, @@ -33,6 +33,27 @@ const showOkModal = (title: string, description: string) => { const getLocalPath = async (...segments: string[]) => path.join(await path.appLocalDataDir(), ...segments); +const stageTraceRuntimeIntoGameDir = async ( + gtasaPath: string, + traceSource: string +): Promise => { + const traceTarget = await path.join(gtasaPath, "omp-socket-trace.dll"); + if (traceSource !== traceTarget) { + await copyFile(traceSource, traceTarget); + } + + const traceSourceDir = await path.dirname(traceSource); + const runtimeSource = await path.join(traceSourceDir, "libwinpthread-1.dll"); + if (await fs.exists(runtimeSource)) { + const runtimeTarget = await path.join(gtasaPath, "libwinpthread-1.dll"); + if (runtimeSource !== runtimeTarget) { + await copyFile(runtimeSource, runtimeTarget); + } + } + + return traceTarget; +}; + export const copySharedFilesIntoGameFolder = async () => { const { gtasaPath } = useSettings.getState(); const shared = await getLocalPath("samp", "shared"); @@ -263,16 +284,6 @@ export const startGame = async ( await path.join(gtasaPath, "omp-socket-trace.dll"), ]; - try { - const launcherDir = await path.executableDir(); - traceCandidates.push(await path.join(launcherDir, "omp-socket-trace.dll")); - } catch (error) { - Log.warn( - "[startGame] Failed to resolve launcher directory for trace DLL lookup:", - error - ); - } - let traceFile = ""; for (const candidate of traceCandidates) { if (await fs.exists(candidate)) { @@ -281,6 +292,25 @@ export const startGame = async ( } } + if (traceFile.length) { + try { + const stagedTraceFile = await stageTraceRuntimeIntoGameDir( + gtasaPath, + traceFile + ); + if (stagedTraceFile) { + traceFile = stagedTraceFile; + } + } catch (error) { + Log.warn( + "[startGame] Failed to stage optional trace runtime into GTA directory:", + error + ); + traceFile = ""; + traceDualstack = false; + } + } + if (!traceFile.length) { traceDualstack = false; From f01096a5d988f71e3dcac8af9be6305fb5f7bed1 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:37:26 +0100 Subject: [PATCH 19/30] launcher: resolve trace DLL only from launcher dir --- src-tauri/src/commands.rs | 15 +++++++++++++++ src-tauri/src/main.rs | 1 + src/utils/game.ts | 36 ++++++++++++++++++++++++++---------- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 8b99e8aa..6aba79a1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -85,6 +85,21 @@ 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() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 88764131..3ca4adc6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -172,6 +172,7 @@ 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, diff --git a/src/utils/game.ts b/src/utils/game.ts index ccfce03f..7c8ee298 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -33,6 +33,11 @@ const showOkModal = (title: string, description: string) => { const getLocalPath = async (...segments: string[]) => path.join(await path.appLocalDataDir(), ...segments); +const getLauncherTracePath = async (): Promise => { + const launcherDir = await invoke("get_launcher_directory"); + return path.join(launcherDir, "omp-socket-trace.dll"); +}; + const stageTraceRuntimeIntoGameDir = async ( gtasaPath: string, traceSource: string @@ -279,17 +284,19 @@ export const startGame = async ( : file ? await getLocalPath(file.path, file.name) : idealSAMPDllPath; - const traceCandidates: string[] = [ - await getLocalPath("omp", "omp-socket-trace.dll"), - await path.join(gtasaPath, "omp-socket-trace.dll"), - ]; - let traceFile = ""; - for (const candidate of traceCandidates) { - if (await fs.exists(candidate)) { - traceFile = candidate; - break; + + try { + const launcherTracePath = await getLauncherTracePath(); + if (await fs.exists(launcherTracePath)) { + traceFile = launcherTracePath; + } else if (launchAddress && isIPv6(launchAddress)) { + Log.warn( + `[startGame] omp-socket-trace.dll not found in launcher directory: ${launcherTracePath}` + ); } + } catch (error) { + Log.warn("[startGame] Failed to resolve launcher directory for trace DLL lookup:", error); } if (traceFile.length) { @@ -322,9 +329,18 @@ export const startGame = async ( ); launchAddress = fallbackIPv4; } else { + let launcherTracePath = "launcher.exe directory"; + try { + launcherTracePath = await getLauncherTracePath(); + } catch (error) { + Log.warn( + "[startGame] Failed to resolve launcher directory while preparing IPv6 compat error:", + error + ); + } showOkModal( "IPv6 compatibility layer missing", - "omp-socket-trace.dll was not found, and no IPv4 fallback address is available for this server." + `omp-socket-trace.dll is missing or unusable at ${launcherTracePath}, and no IPv4 fallback address is available for this server.` ); showPrompt(true); setServer(server); From 57c13ea7f85d0c113f10b88551afdd15eb1ece2b Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:45:42 +0100 Subject: [PATCH 20/30] launcher: hook dynamic winsock resolution in trace shim --- tools/win-socket-trace/wsock_trace.cpp | 260 +++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 11 deletions(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 672ff1ce..50836cd7 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -36,6 +36,11 @@ using sendto_fn_t = int(WSAAPI*)(SOCKET, const char*, int, int, const sockaddr*, using recvfrom_fn_t = int(WSAAPI*)(SOCKET, char*, int, int, sockaddr*, int*); 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); +using loadlibraryw_fn_t = HMODULE(WINAPI*)(LPCWSTR); +using loadlibraryexa_fn_t = HMODULE(WINAPI*)(LPCSTR, HANDLE, DWORD); +using loadlibraryexw_fn_t = HMODULE(WINAPI*)(LPCWSTR, HANDLE, DWORD); static socket_fn_t g_real_socket = nullptr; static wsasocketa_fn_t g_real_wsasocketa = nullptr; @@ -48,6 +53,11 @@ static sendto_fn_t g_real_sendto = nullptr; static recvfrom_fn_t g_real_recvfrom = 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 loadlibraryw_fn_t g_real_loadlibraryw = nullptr; +static loadlibraryexa_fn_t g_real_loadlibraryexa = nullptr; +static loadlibraryexw_fn_t g_real_loadlibraryexw = nullptr; static std::mutex g_log_mutex; static std::once_flag g_log_once; @@ -60,6 +70,7 @@ 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) { @@ -262,6 +273,75 @@ static std::string format_sockaddr(const sockaddr* sa, int salen) { return out; } +static bool is_ordinal_proc_name(LPCSTR name) { + return reinterpret_cast(name) <= 0xFFFF; +} + +static std::string narrow_wide_string(LPCWSTR value) { + if (!value) { + return "(null)"; + } + + int needed = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); + if (needed <= 0) { + return "(wide-conversion-failed)"; + } + + std::string out; + out.resize((size_t)needed); + if (WideCharToMultiByte( + CP_UTF8, + 0, + value, + -1, + out.empty() ? nullptr : &out[0], + needed, + nullptr, + nullptr) <= 0) { + return "(wide-conversion-failed)"; + } + if (!out.empty() && out.back() == '\0') { + out.pop_back(); + } + return out; +} + +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 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"); +} + 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) { @@ -350,14 +430,53 @@ static bool copy_or_translate_addr(const sockaddr_storage& src, int src_len, soc template static void resolve_real(T& fn, HMODULE ws2, const char* name) { if (!fn && ws2) { - fn = reinterpret_cast(GetProcAddress(ws2, name)); + 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"); + resolve_real(g_real_loadlibraryw, kernel, "LoadLibraryW"); + resolve_real(g_real_loadlibraryexa, kernel, "LoadLibraryExA"); + resolve_real(g_real_loadlibraryexw, kernel, "LoadLibraryExW"); + + if (!g_real_loadlibrarya) { + g_real_loadlibrarya = reinterpret_cast(resolve_proc(kernel, "LoadLibraryA")); + } + if (!g_real_loadlibraryw) { + g_real_loadlibraryw = reinterpret_cast(resolve_proc(kernel, "LoadLibraryW")); + } + if (!g_real_loadlibraryexa) { + g_real_loadlibraryexa = + reinterpret_cast(resolve_proc(kernel, "LoadLibraryExA")); + } + if (!g_real_loadlibraryexw) { + g_real_loadlibraryexw = + reinterpret_cast(resolve_proc(kernel, "LoadLibraryExW")); } } static void resolve_real_functions() { + resolve_kernel_functions(); HMODULE ws2 = GetModuleHandleA("ws2_32.dll"); if (!ws2) { - ws2 = LoadLibraryA("ws2_32.dll"); + ws2 = g_real_loadlibrarya ? g_real_loadlibrarya("ws2_32.dll") : LoadLibraryA("ws2_32.dll"); } resolve_real(g_real_socket, ws2, "socket"); resolve_real(g_real_wsasocketa, ws2, "WSASocketA"); @@ -412,16 +531,19 @@ static bool patch_iat_for_module( base + (imp->OriginalFirstThunk ? imp->OriginalFirstThunk : imp->FirstThunk)); for (; orig->u1.AddressOfData; ++orig, ++thunk) { + auto current = reinterpret_cast((uintptr_t)thunk->u1.Function); if (IMAGE_SNAP_BY_ORDINAL(orig->u1.Ordinal)) { - continue; - } - - auto* by_name = reinterpret_cast(base + orig->u1.AddressOfData); - if (strcmp(reinterpret_cast(by_name->Name), func_name) != 0) { - continue; + if (!original_store || *original_store == nullptr || current != *original_store) { + continue; + } + } else { + auto* by_name = + reinterpret_cast(base + orig->u1.AddressOfData); + if (strcmp(reinterpret_cast(by_name->Name), func_name) != 0) { + continue; + } } - auto current = reinterpret_cast((uintptr_t)thunk->u1.Function); if (current == replacement) { patched = true; continue; @@ -506,8 +628,13 @@ static int WSAAPI hook_recvfrom( int* fromlen); 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 HMODULE WINAPI hook_loadlibrarya(LPCSTR name); +static HMODULE WINAPI hook_loadlibraryw(LPCWSTR name); +static HMODULE WINAPI hook_loadlibraryexa(LPCSTR name, HANDLE file, DWORD flags); +static HMODULE WINAPI hook_loadlibraryexw(LPCWSTR name, HANDLE file, DWORD flags); -static HookDef kHooks[] = { +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}, @@ -521,13 +648,65 @@ static HookDef kHooks[] = { {"getpeername", (void*)&hook_getpeername, (void**)&g_real_getpeername}, }; +static HookDef kKernelHooks[] = { + {"GetProcAddress", (void*)&hook_getprocaddress, (void**)&g_real_getprocaddress}, + {"LoadLibraryA", (void*)&hook_loadlibrarya, (void**)&g_real_loadlibrarya}, + {"LoadLibraryW", (void*)&hook_loadlibraryw, (void**)&g_real_loadlibraryw}, + {"LoadLibraryExA", (void*)&hook_loadlibraryexa, (void**)&g_real_loadlibraryexa}, + {"LoadLibraryExW", (void*)&hook_loadlibraryexw, (void**)&g_real_loadlibraryexw}, +}; + +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 : kHooks) { + 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); } + + 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() { @@ -546,6 +725,63 @@ static void patch_loaded_modules() { 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 HMODULE finish_loadlibrary(HMODULE mod, const char* api_name, const char* target_name) { + if (mod) { + apply_hooks_for_module(mod); + } + + log_line( + "%s(name=%s) => module=%p", + api_name, + target_name ? target_name : "(null)", + mod); + return mod; +} + +static HMODULE WINAPI hook_loadlibrarya(LPCSTR name) { + resolve_kernel_functions(); + HMODULE mod = g_real_loadlibrarya ? g_real_loadlibrarya(name) : nullptr; + return finish_loadlibrary(mod, "LoadLibraryA", name); +} + +static HMODULE WINAPI hook_loadlibraryw(LPCWSTR name) { + resolve_kernel_functions(); + HMODULE mod = g_real_loadlibraryw ? g_real_loadlibraryw(name) : nullptr; + std::string narrow = narrow_wide_string(name); + return finish_loadlibrary(mod, "LoadLibraryW", narrow.c_str()); +} + +static HMODULE WINAPI hook_loadlibraryexa(LPCSTR name, HANDLE file, DWORD flags) { + resolve_kernel_functions(); + HMODULE mod = g_real_loadlibraryexa ? g_real_loadlibraryexa(name, file, flags) : nullptr; + return finish_loadlibrary(mod, "LoadLibraryExA", name); +} + +static HMODULE WINAPI hook_loadlibraryexw(LPCWSTR name, HANDLE file, DWORD flags) { + resolve_kernel_functions(); + HMODULE mod = g_real_loadlibraryexw ? g_real_loadlibraryexw(name, file, flags) : nullptr; + std::string narrow = narrow_wide_string(name); + return finish_loadlibrary(mod, "LoadLibraryExW", narrow.c_str()); +} + static SOCKET WSAAPI hook_socket(int af, int type, int protocol) { resolve_real_functions(); int requested_af = af; @@ -999,6 +1235,7 @@ static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen) { } 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; @@ -1050,6 +1287,7 @@ static DWORD WINAPI init_worker(void*) { 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) { From 8a614d0ab2945bebfd482eae6d833b11eedb17a4 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:53:23 +0100 Subject: [PATCH 21/30] launcher: drop loadlibrary hooks from trace shim --- tools/win-socket-trace/wsock_trace.cpp | 96 -------------------------- 1 file changed, 96 deletions(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 50836cd7..0c3bad66 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -38,9 +38,6 @@ 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); -using loadlibraryw_fn_t = HMODULE(WINAPI*)(LPCWSTR); -using loadlibraryexa_fn_t = HMODULE(WINAPI*)(LPCSTR, HANDLE, DWORD); -using loadlibraryexw_fn_t = HMODULE(WINAPI*)(LPCWSTR, HANDLE, DWORD); static socket_fn_t g_real_socket = nullptr; static wsasocketa_fn_t g_real_wsasocketa = nullptr; @@ -55,9 +52,6 @@ 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 loadlibraryw_fn_t g_real_loadlibraryw = nullptr; -static loadlibraryexa_fn_t g_real_loadlibraryexa = nullptr; -static loadlibraryexw_fn_t g_real_loadlibraryexw = nullptr; static std::mutex g_log_mutex; static std::once_flag g_log_once; @@ -277,35 +271,6 @@ static bool is_ordinal_proc_name(LPCSTR name) { return reinterpret_cast(name) <= 0xFFFF; } -static std::string narrow_wide_string(LPCWSTR value) { - if (!value) { - return "(null)"; - } - - int needed = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); - if (needed <= 0) { - return "(wide-conversion-failed)"; - } - - std::string out; - out.resize((size_t)needed); - if (WideCharToMultiByte( - CP_UTF8, - 0, - value, - -1, - out.empty() ? nullptr : &out[0], - needed, - nullptr, - nullptr) <= 0) { - return "(wide-conversion-failed)"; - } - if (!out.empty() && out.back() == '\0') { - out.pop_back(); - } - return out; -} - static const char* module_basename(const char* path) { if (!path) { return ""; @@ -452,24 +417,10 @@ static void resolve_kernel_functions() { FARPROC(WINAPI *resolve_proc)(HMODULE, LPCSTR) = g_real_getprocaddress ? g_real_getprocaddress : ::GetProcAddress; resolve_real(g_real_loadlibrarya, kernel, "LoadLibraryA"); - resolve_real(g_real_loadlibraryw, kernel, "LoadLibraryW"); - resolve_real(g_real_loadlibraryexa, kernel, "LoadLibraryExA"); - resolve_real(g_real_loadlibraryexw, kernel, "LoadLibraryExW"); if (!g_real_loadlibrarya) { g_real_loadlibrarya = reinterpret_cast(resolve_proc(kernel, "LoadLibraryA")); } - if (!g_real_loadlibraryw) { - g_real_loadlibraryw = reinterpret_cast(resolve_proc(kernel, "LoadLibraryW")); - } - if (!g_real_loadlibraryexa) { - g_real_loadlibraryexa = - reinterpret_cast(resolve_proc(kernel, "LoadLibraryExA")); - } - if (!g_real_loadlibraryexw) { - g_real_loadlibraryexw = - reinterpret_cast(resolve_proc(kernel, "LoadLibraryExW")); - } } static void resolve_real_functions() { @@ -629,10 +580,6 @@ static int WSAAPI hook_recvfrom( 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 HMODULE WINAPI hook_loadlibrarya(LPCSTR name); -static HMODULE WINAPI hook_loadlibraryw(LPCWSTR name); -static HMODULE WINAPI hook_loadlibraryexa(LPCSTR name, HANDLE file, DWORD flags); -static HMODULE WINAPI hook_loadlibraryexw(LPCWSTR name, HANDLE file, DWORD flags); static HookDef kSocketHooks[] = { {"socket", (void*)&hook_socket, (void**)&g_real_socket}, @@ -650,10 +597,6 @@ static HookDef kSocketHooks[] = { static HookDef kKernelHooks[] = { {"GetProcAddress", (void*)&hook_getprocaddress, (void**)&g_real_getprocaddress}, - {"LoadLibraryA", (void*)&hook_loadlibrarya, (void**)&g_real_loadlibrarya}, - {"LoadLibraryW", (void*)&hook_loadlibraryw, (void**)&g_real_loadlibraryw}, - {"LoadLibraryExA", (void*)&hook_loadlibraryexa, (void**)&g_real_loadlibraryexa}, - {"LoadLibraryExW", (void*)&hook_loadlibraryexw, (void**)&g_real_loadlibraryexw}, }; static const HookDef* find_socket_hook(const char* proc_name) { @@ -743,45 +686,6 @@ static FARPROC WINAPI hook_getprocaddress(HMODULE mod, LPCSTR proc_name) { return g_real_getprocaddress ? g_real_getprocaddress(mod, proc_name) : nullptr; } -static HMODULE finish_loadlibrary(HMODULE mod, const char* api_name, const char* target_name) { - if (mod) { - apply_hooks_for_module(mod); - } - - log_line( - "%s(name=%s) => module=%p", - api_name, - target_name ? target_name : "(null)", - mod); - return mod; -} - -static HMODULE WINAPI hook_loadlibrarya(LPCSTR name) { - resolve_kernel_functions(); - HMODULE mod = g_real_loadlibrarya ? g_real_loadlibrarya(name) : nullptr; - return finish_loadlibrary(mod, "LoadLibraryA", name); -} - -static HMODULE WINAPI hook_loadlibraryw(LPCWSTR name) { - resolve_kernel_functions(); - HMODULE mod = g_real_loadlibraryw ? g_real_loadlibraryw(name) : nullptr; - std::string narrow = narrow_wide_string(name); - return finish_loadlibrary(mod, "LoadLibraryW", narrow.c_str()); -} - -static HMODULE WINAPI hook_loadlibraryexa(LPCSTR name, HANDLE file, DWORD flags) { - resolve_kernel_functions(); - HMODULE mod = g_real_loadlibraryexa ? g_real_loadlibraryexa(name, file, flags) : nullptr; - return finish_loadlibrary(mod, "LoadLibraryExA", name); -} - -static HMODULE WINAPI hook_loadlibraryexw(LPCWSTR name, HANDLE file, DWORD flags) { - resolve_kernel_functions(); - HMODULE mod = g_real_loadlibraryexw ? g_real_loadlibraryexw(name, file, flags) : nullptr; - std::string narrow = narrow_wide_string(name); - return finish_loadlibrary(mod, "LoadLibraryExW", narrow.c_str()); -} - static SOCKET WSAAPI hook_socket(int af, int type, int protocol) { resolve_real_functions(); int requested_af = af; From 5bfc211fc5882f8f9fe34b19a83128f1466ecea3 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 22:59:15 +0100 Subject: [PATCH 22/30] launcher: bind dualstack shim sockets to native v6 any --- tools/win-socket-trace/wsock_trace.cpp | 88 +++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 0c3bad66..5665a8de 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -317,6 +317,25 @@ static bool is_v4_mapped(const sockaddr_in6& in6) { 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; @@ -328,6 +347,24 @@ static sockaddr_in6 v4_to_mapped(const sockaddr_in& in4) { 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; @@ -341,6 +378,33 @@ static bool mapped_to_v4(const sockaddr_in6& in6, sockaddr_in* out4) { 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; @@ -371,7 +435,12 @@ static bool should_translate_to_v6(SOCKET s) { 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) { +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; } @@ -379,7 +448,8 @@ static bool copy_or_translate_addr(const sockaddr_storage& src, int src_len, soc if (src.ss_family == AF_INET6 && src_len >= (int)sizeof(sockaddr_in6) && *dst_len >= (int)sizeof(sockaddr_in)) { sockaddr_in out4{}; - if (mapped_to_v4(*reinterpret_cast(&src), &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; @@ -927,13 +997,13 @@ static int WSAAPI hook_bind(SOCKET s, const sockaddr* name, int namelen) { const sockaddr* real_name = name; int real_len = namelen; - sockaddr_in6 mapped{}; + sockaddr_in6 translated_addr{}; bool translated = false; if (name && name->sa_family == AF_INET && should_translate_to_v6(s)) { - mapped = v4_to_mapped(*reinterpret_cast(name)); - real_name = reinterpret_cast(&mapped); - real_len = (int)sizeof(mapped); + translated_addr = v4_bind_to_v6(*reinterpret_cast(name)); + real_name = reinterpret_cast(&translated_addr); + real_len = (int)sizeof(translated_addr); translated = true; } @@ -1036,7 +1106,7 @@ static int WSAAPI hook_recvfrom( int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; if (rc != SOCKET_ERROR) { - copy_or_translate_addr(tmp, tmp_len, from, fromlen); + copy_or_translate_addr(tmp, tmp_len, from, fromlen, true); } log_line( @@ -1080,7 +1150,7 @@ static int WSAAPI hook_getsockname(SOCKET s, sockaddr* name, int* namelen) { : SOCKET_ERROR; int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; if (rc != SOCKET_ERROR) { - copy_or_translate_addr(tmp, tmp_len, name, namelen); + copy_or_translate_addr(tmp, tmp_len, name, namelen, false); } log_line( @@ -1122,7 +1192,7 @@ static int WSAAPI hook_getpeername(SOCKET s, sockaddr* name, int* namelen) { : SOCKET_ERROR; int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; if (rc != SOCKET_ERROR) { - copy_or_translate_addr(tmp, tmp_len, name, namelen); + copy_or_translate_addr(tmp, tmp_len, name, namelen, true); } log_line( From 2148fa9b7b458cbe47a05f68aa8005bb1ac01a54 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 23:30:25 +0100 Subject: [PATCH 23/30] launcher: hook wsarecvfrom in trace shim --- tools/win-socket-trace/wsock_trace.cpp | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 5665a8de..ad1328f5 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -34,6 +34,16 @@ using wsaconnect_fn_t = 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 recvfrom_fn_t = int(WSAAPI*)(SOCKET, char*, int, int, sockaddr*, int*); +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); @@ -48,6 +58,7 @@ 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 recvfrom_fn_t g_real_recvfrom = 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; @@ -508,6 +519,7 @@ static void resolve_real_functions() { resolve_real(g_real_bind, ws2, "bind"); resolve_real(g_real_sendto, ws2, "sendto"); resolve_real(g_real_recvfrom, ws2, "recvfrom"); + resolve_real(g_real_wsarecvfrom, ws2, "WSARecvFrom"); resolve_real(g_real_getsockname, ws2, "getsockname"); resolve_real(g_real_getpeername, ws2, "getpeername"); } @@ -647,6 +659,16 @@ static int WSAAPI hook_recvfrom( int flags, sockaddr* from, int* fromlen); +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_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); @@ -661,6 +683,7 @@ static HookDef kSocketHooks[] = { {"bind", (void*)&hook_bind, (void**)&g_real_bind}, {"sendto", (void*)&hook_sendto, (void**)&g_real_sendto}, {"recvfrom", (void*)&hook_recvfrom, (void**)&g_real_recvfrom}, + {"WSARecvFrom", (void*)&hook_wsarecvfrom, (void**)&g_real_wsarecvfrom}, {"getsockname", (void*)&hook_getsockname, (void**)&g_real_getsockname}, {"getpeername", (void*)&hook_getpeername, (void**)&g_real_getpeername}, }; @@ -1124,6 +1147,87 @@ static int WSAAPI hook_recvfrom( 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) => rc=%d err=%d from=%s", + (unsigned long long)s, + (unsigned long)buffer_count, + overlapped ? 1 : 0, + 0, + rc, + err, + from && fromlen ? format_sockaddr(from, *fromlen).c_str() : "(null)"); + 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) => 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, + 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; From 13255542ee58e2f5b426b6d17415f1b6a80f3811 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 5 Mar 2026 23:53:01 +0100 Subject: [PATCH 24/30] launcher: hook recv and wsarecv in trace shim --- tools/win-socket-trace/wsock_trace.cpp | 161 +++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 7 deletions(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index ad1328f5..f9991162 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -33,7 +33,16 @@ 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 wsarecv_fn_t = int(WSAAPI*)( + SOCKET, + LPWSABUF, + DWORD, + LPDWORD, + LPDWORD, + LPWSAOVERLAPPED, + LPWSAOVERLAPPED_COMPLETION_ROUTINE); using wsarecvfrom_fn_t = int(WSAAPI*)( SOCKET, LPWSABUF, @@ -57,7 +66,9 @@ 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 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; @@ -309,6 +320,61 @@ static std::string module_name_for_handle(HMODULE mod) { 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; @@ -518,7 +584,9 @@ static void resolve_real_functions() { 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_wsarecv, ws2, "WSARecv"); resolve_real(g_real_wsarecvfrom, ws2, "WSARecvFrom"); resolve_real(g_real_getsockname, ws2, "getsockname"); resolve_real(g_real_getpeername, ws2, "getpeername"); @@ -659,6 +727,15 @@ static int WSAAPI hook_recvfrom( 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, @@ -682,7 +759,9 @@ static HookDef kSocketHooks[] = { {"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}, + {"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}, @@ -1080,12 +1159,35 @@ static int WSAAPI hook_sendto( int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; log_line( - "sendto(sock=%llu,len=%d,to=%s,translated=%d,forced_remote=%d) => rc=%d err=%d", + "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); @@ -1109,10 +1211,11 @@ static int WSAAPI hook_recvfrom( 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) => rc=%d err=%d", + "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) { @@ -1133,11 +1236,49 @@ static int WSAAPI hook_recvfrom( } log_line( - "recvfrom(sock=%llu,len=%d,raw_from=%s,translated=%d) => rc=%d err=%d", + "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_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); @@ -1175,14 +1316,17 @@ static int WSAAPI hook_wsarecvfrom( : SOCKET_ERROR; int err = (rc == SOCKET_ERROR) ? WSAGetLastError() : 0; log_line( - "WSARecvFrom(sock=%llu,buffers=%lu,overlapped=%d,translated=%d) => rc=%d err=%d from=%s", + "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, - from && fromlen ? format_sockaddr(from, *fromlen).c_str() : "(null)"); + err); if (rc == SOCKET_ERROR) { WSASetLastError(err); } @@ -1212,13 +1356,16 @@ static int WSAAPI hook_wsarecvfrom( 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) => rc=%d err=%d", + "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); From 6d8d8090d91ae8339c67e1d0404d19cb59d71fc3 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 6 Mar 2026 00:03:56 +0100 Subject: [PATCH 25/30] launcher: clarify trace shim dependency failures --- src-tauri/src/injector.rs | 21 ++++++++++++++++++--- src/utils/game.ts | 16 ++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 660a0171..10181b0b 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -15,6 +15,19 @@ 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, @@ -102,9 +115,10 @@ pub async fn run_samp( Ok(p) => { if !trace_file.is_empty() { 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, e + trace_file, error_text ); } } @@ -202,13 +216,14 @@ pub fn inject_dll(child: u32, dll_path: &str, times: u32, waiting_for_vorbis: bo 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 { @@ -216,7 +231,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/utils/game.ts b/src/utils/game.ts index 7c8ee298..90d1da86 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -49,11 +49,15 @@ const stageTraceRuntimeIntoGameDir = async ( const traceSourceDir = await path.dirname(traceSource); const runtimeSource = await path.join(traceSourceDir, "libwinpthread-1.dll"); - if (await fs.exists(runtimeSource)) { - const runtimeTarget = await path.join(gtasaPath, "libwinpthread-1.dll"); - if (runtimeSource !== runtimeTarget) { - await copyFile(runtimeSource, runtimeTarget); - } + if (!(await fs.exists(runtimeSource))) { + throw new Error( + `Missing trace runtime dependency next to omp-socket-trace.dll: ${runtimeSource}` + ); + } + + const runtimeTarget = await path.join(gtasaPath, "libwinpthread-1.dll"); + if (runtimeSource !== runtimeTarget) { + await copyFile(runtimeSource, runtimeTarget); } return traceTarget; @@ -310,7 +314,7 @@ export const startGame = async ( } } catch (error) { Log.warn( - "[startGame] Failed to stage optional trace runtime into GTA directory:", + "[startGame] Failed to stage optional trace runtime into GTA directory. The trace shim requires omp-socket-trace.dll and libwinpthread-1.dll in the launcher directory:", error ); traceFile = ""; From f8ed0061793ccc8a330c963ddce861e3407f71a3 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 6 Mar 2026 09:28:50 +0100 Subject: [PATCH 26/30] launcher: log and bound DLL injection waits --- src-tauri/src/constants.rs | 1 + src-tauri/src/injector.rs | 55 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 1ab001bd..886768e9 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -19,6 +19,7 @@ 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; diff --git a/src-tauri/src/injector.rs b/src-tauri/src/injector.rs index 10181b0b..3e73bae3 100644 --- a/src-tauri/src/injector.rs +++ b/src-tauri/src/injector.rs @@ -109,11 +109,22 @@ pub async fn run_samp( } } + 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!( @@ -122,9 +133,11 @@ pub async fn run_samp( ); } } + 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(()) @@ -180,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; @@ -202,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); } } } @@ -213,6 +258,12 @@ 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) => { From 63a27e0754c56231cc2ea7da348f4ece8991dfe4 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 6 Mar 2026 10:05:12 +0100 Subject: [PATCH 27/30] launcher: statically link trace shim C++ runtime --- tools/win-socket-trace/CMakeLists.txt | 4 ++++ tools/win-socket-trace/README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/win-socket-trace/CMakeLists.txt b/tools/win-socket-trace/CMakeLists.txt index b80d9ff9..ffe4b7d9 100644 --- a/tools/win-socket-trace/CMakeLists.txt +++ b/tools/win-socket-trace/CMakeLists.txt @@ -9,3 +9,7 @@ set_target_properties(omp-socket-trace PROPERTIES 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 index d5be2bfd..3787ded5 100644 --- a/tools/win-socket-trace/README.md +++ b/tools/win-socket-trace/README.md @@ -35,7 +35,7 @@ cmake --build build-mingw32 -j ### Direct g++ ```bash -i686-w64-mingw32-g++ -O2 -std=c++17 -shared -o omp-socket-trace.dll wsock_trace.cpp -lws2_32 +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 From 1496ea7d99cab6f1d4f97012156d6742d1b1767a Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 6 Mar 2026 10:46:32 +0100 Subject: [PATCH 28/30] launcher: patch ordinal winsock hooks using import export resolution --- tools/win-socket-trace/wsock_trace.cpp | 63 +++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index f9991162..44c1f22c 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace { @@ -81,6 +82,8 @@ 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; @@ -592,6 +595,38 @@ static void resolve_real_functions() { 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, @@ -627,16 +662,41 @@ static bool patch_iat_for_module( 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 (!original_store || *original_store == nullptr || current != *original_store) { + 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); @@ -675,6 +735,7 @@ static bool patch_iat_for_module( &thunk->u1.Function, sizeof(thunk->u1.Function)); patched = true; + log_patch_once(mod, dll, func_name, matched_by_ordinal); } } From ba41c8ef5268fc566f35256f8cf72547cb52a85d Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 6 Mar 2026 11:00:36 +0100 Subject: [PATCH 29/30] launcher: hook mswsock WSARecvEx in trace shim --- tools/win-socket-trace/wsock_trace.cpp | 40 +++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tools/win-socket-trace/wsock_trace.cpp b/tools/win-socket-trace/wsock_trace.cpp index 44c1f22c..eea8134b 100644 --- a/tools/win-socket-trace/wsock_trace.cpp +++ b/tools/win-socket-trace/wsock_trace.cpp @@ -36,6 +36,7 @@ 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, @@ -69,6 +70,7 @@ 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; @@ -384,7 +386,8 @@ static bool module_name_equals(HMODULE mod, const char* expected) { } static bool is_ws2_family_module(HMODULE mod) { - return module_name_equals(mod, "ws2_32.dll") || module_name_equals(mod, "wsock32.dll"); + 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) { @@ -579,6 +582,10 @@ static void resolve_real_functions() { 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"); @@ -589,6 +596,7 @@ static void resolve_real_functions() { 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"); @@ -807,6 +815,7 @@ static int WSAAPI hook_wsarecvfrom( 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); @@ -822,6 +831,7 @@ static HookDef kSocketHooks[] = { {"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}, @@ -851,6 +861,9 @@ static void apply_hooks_for_module(HMODULE mod) { 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) { @@ -1312,6 +1325,31 @@ static int WSAAPI hook_recvfrom( 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, From 2a1398cebf9be0d008572080dec68e815ccec7cc Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Sat, 7 Mar 2026 02:09:33 +0100 Subject: [PATCH 30/30] launcher: add IPv6 local UDP proxy with legacy RakNet remap --- src-tauri/src/ipv6_proxy.rs | 538 ++++++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 38 ++- src/utils/game.ts | 190 ++++--------- 3 files changed, 631 insertions(+), 135 deletions(-) create mode 100644 src-tauri/src/ipv6_proxy.rs 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 3ca4adc6..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/{}", @@ -128,8 +160,8 @@ async fn handle_cli_args() -> Result<()> { run_samp( args.name.as_ref().unwrap(), - args.host.as_ref().unwrap(), - args.port.unwrap(), + &launch_host, + launch_port, gamepath, &format!("{}/{}", gamepath, SAMP_DLL), "", @@ -177,6 +209,8 @@ async fn run_tauri_app() -> Result<()> { 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/utils/game.ts b/src/utils/game.ts index 90d1da86..3e102bbb 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -1,6 +1,6 @@ import { fs, invoke, path, process, shell } from "@tauri-apps/api"; import { open, save } from "@tauri-apps/api/dialog"; -import { copyFile, exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; +import { exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; import { t } from "i18next"; import { IN_GAME, @@ -19,7 +19,7 @@ import { Log } from "./logger"; import { PING_TIMEOUT_VALUE } from "./query"; import { sc } from "./sizeScaler"; import { Server } from "./types"; -import { isIPv6, normalizeIPv6 } from "./validation"; +import { isIPv6 } from "./validation"; const showOkModal = (title: string, description: string) => { const { showMessageBox, hideMessageBox } = useMessageBox.getState(); @@ -33,34 +33,31 @@ const showOkModal = (title: string, description: string) => { const getLocalPath = async (...segments: string[]) => path.join(await path.appLocalDataDir(), ...segments); -const getLauncherTracePath = async (): Promise => { - const launcherDir = await invoke("get_launcher_directory"); - return path.join(launcherDir, "omp-socket-trace.dll"); -}; - -const stageTraceRuntimeIntoGameDir = async ( - gtasaPath: string, - traceSource: string -): Promise => { - const traceTarget = await path.join(gtasaPath, "omp-socket-trace.dll"); - if (traceSource !== traceTarget) { - await copyFile(traceSource, traceTarget); - } +interface ProxyInfo { + host: string; + port: number; +} - const traceSourceDir = await path.dirname(traceSource); - const runtimeSource = await path.join(traceSourceDir, "libwinpthread-1.dll"); - if (!(await fs.exists(runtimeSource))) { - throw new Error( - `Missing trace runtime dependency next to omp-socket-trace.dll: ${runtimeSource}` - ); +const stopIpv6ProxySilently = async () => { + try { + await invoke("stop_ipv6_proxy"); + } catch { + // Best effort cleanup. } +}; - const runtimeTarget = await path.join(gtasaPath, "libwinpthread-1.dll"); - if (runtimeSource !== runtimeTarget) { - await copyFile(runtimeSource, runtimeTarget); +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; } - - return traceTarget; }; export const copySharedFilesIntoGameFolder = async () => { @@ -105,52 +102,51 @@ export const startGame = async ( const { setSelected } = useServers.getState(); const resolvedAddress = (await getIpAddress(server.ip)) ?? server.ip; let launchAddress = resolvedAddress; - let traceDualstack = false; - let traceRemoteIp = ""; - let traceRemotePort = 0; - let injectAddress = launchAddress; + let launchPort = server.port; if (resolvedAddress && isIPv6(resolvedAddress)) { - const normalizedIPv6 = normalizeIPv6(resolvedAddress); - let ipv6ProbeOk = false; - - try { - ipv6ProbeOk = await invoke("probe_ipv6_query", { - host: normalizedIPv6, - port: server.port, - }); - traceDualstack = ipv6ProbeOk; - } catch (error) { - Log.warn("[startGame] IPv6 probe failed unexpectedly:", error); - } - - if (!ipv6ProbeOk) { + 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; - traceDualstack = false; + launchPort = server.port; + await stopIpv6ProxySilently(); Log.warn( - `[startGame] IPv6 probe failed for ${normalizedIPv6}:${server.port}, falling back to IPv4 ${fallbackIPv4}` + `[startGame] IPv6 proxy start failed for ${normalizedIPv6}:${server.port}, falling back to IPv4 ${fallbackIPv4}:${server.port}` ); } else { - Log.warn( - `[startGame] IPv6 probe failed for ${normalizedIPv6}:${server.port} and no IPv4 fallback was found` + 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 && isIPv6(launchAddress) - ? `[${normalizeIPv6(launchAddress)}]` - : launchAddress; + const connectAddress = launchAddress; if (IN_GAME) { invoke("send_message_to_game", { id: IN_GAME_PROCESS_ID, message: password.length - ? `connect:${connectAddress}:${server.port}:${nickname}:${password}` - : `connect:${connectAddress}:${server.port}:${nickname}`, + ? `connect:${connectAddress}:${launchPort}:${nickname}:${password}` + : `connect:${connectAddress}:${launchPort}:${nickname}`, }); return; } @@ -288,89 +284,17 @@ export const startGame = async ( : file ? await getLocalPath(file.path, file.name) : idealSAMPDllPath; - let traceFile = ""; - - try { - const launcherTracePath = await getLauncherTracePath(); - if (await fs.exists(launcherTracePath)) { - traceFile = launcherTracePath; - } else if (launchAddress && isIPv6(launchAddress)) { - Log.warn( - `[startGame] omp-socket-trace.dll not found in launcher directory: ${launcherTracePath}` - ); - } - } catch (error) { - Log.warn("[startGame] Failed to resolve launcher directory for trace DLL lookup:", error); - } - - if (traceFile.length) { - try { - const stagedTraceFile = await stageTraceRuntimeIntoGameDir( - gtasaPath, - traceFile - ); - if (stagedTraceFile) { - traceFile = stagedTraceFile; - } - } catch (error) { - Log.warn( - "[startGame] Failed to stage optional trace runtime into GTA directory. The trace shim requires omp-socket-trace.dll and libwinpthread-1.dll in the launcher directory:", - error - ); - traceFile = ""; - traceDualstack = false; - } - } - - if (!traceFile.length) { - traceDualstack = false; - - if (launchAddress && isIPv6(launchAddress)) { - const fallbackIPv4 = await getIpAddress(server.ip, "ipv4"); - if (fallbackIPv4 && !isIPv6(fallbackIPv4)) { - Log.warn( - `[startGame] IPv6 launch requires omp-socket-trace.dll; falling back to IPv4 ${fallbackIPv4}` - ); - launchAddress = fallbackIPv4; - } else { - let launcherTracePath = "launcher.exe directory"; - try { - launcherTracePath = await getLauncherTracePath(); - } catch (error) { - Log.warn( - "[startGame] Failed to resolve launcher directory while preparing IPv6 compat error:", - error - ); - } - showOkModal( - "IPv6 compatibility layer missing", - `omp-socket-trace.dll is missing or unusable at ${launcherTracePath}, and no IPv4 fallback address is available for this server.` - ); - showPrompt(true); - setServer(server); - return; - } - } - } - - if (launchAddress && isIPv6(launchAddress) && traceDualstack && traceFile.length) { - traceRemoteIp = normalizeIPv6(launchAddress); - traceRemotePort = server.port; - injectAddress = "127.0.0.1"; - } else { - injectAddress = launchAddress; - } invoke("inject", { name: nickname, - ip: injectAddress, - port: server.port, + ip: launchAddress, + port: launchPort, exe: gtasaPath, dll: ourSAMPDllPath, - traceFile, - traceDualstack, - traceRemoteIp, - traceRemotePort, + traceFile: "", + traceDualstack: false, + traceRemoteIp: "", + traceRemotePort: 0, ompFile: await getLocalPath("omp", "omp-client.dll"), password, customGameExe,