diff --git a/Netch/Forms/MainForm.cs b/Netch/Forms/MainForm.cs index ef05645016..fff31b6077 100644 --- a/Netch/Forms/MainForm.cs +++ b/Netch/Forms/MainForm.cs @@ -24,6 +24,7 @@ public partial class MainForm : Form #region Start private readonly Dictionary _mainFormText = new(); + private CancellationTokenSource? _mainCancellationTokenSource; private bool _textRecorded; @@ -517,73 +518,84 @@ private void fAQToolStripMenuItem_Click(object sender, EventArgs e) #region ControlButton private async void ControlButton_Click(object? sender, EventArgs? e) +{ + if (!IsWaiting()) { - if (!IsWaiting()) - { - await StopCoreAsync(); - return; - } + await StopCoreAsync(); + return; + } - Configuration.SaveAsync().Forget(); + Configuration.SaveAsync().Forget(); - // 服务器、模式 需选择 - if (ServerComboBox.SelectedItem is not Server server) - { - MessageBoxX.Show(i18N.Translate("Please select a server first")); - return; - } + // 服务器、模式 需选择 + if (ServerComboBox.SelectedItem is not Server server) + { + MessageBoxX.Show(i18N.Translate("Please select a server first")); + return; + } - if (ModeComboBox.SelectedItem is not Mode mode) - { - MessageBoxX.Show(i18N.Translate("Please select a mode first")); - return; - } + if (ModeComboBox.SelectedItem is not Mode mode) + { + MessageBoxX.Show(i18N.Translate("Please select a mode first")); + return; + } - State = State.Starting; + State = State.Starting; + + // Create new cancellation token for this session + _mainCancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = _mainCancellationTokenSource.Token; - try - { - await MainController.StartAsync(server, mode); - } - catch (Exception exception) - { - State = State.Stopped; - StatusText(i18N.Translate("Start failed")); - MessageBoxX.Show(exception.Message, LogLevel.ERROR); - return; - } + try + { + await MainController.StartAsync(server, mode); + } + catch (Exception exception) + { + State = State.Stopped; + StatusText(i18N.Translate("Start failed")); + MessageBoxX.Show(exception.Message, LogLevel.ERROR); + return; + } - State = State.Started; + State = State.Started; - Task.Run(Bandwidth.NetTraffic).Forget(); - DiscoveryNatTypeAsync().Forget(); - HttpConnectAsync().Forget(); + Task.Run(() => Bandwidth.NetTraffic(cancellationToken), cancellationToken).Forget(); + DiscoveryNatTypeAsync().Forget(); + HttpConnectAsync().Forget(); - if (Global.Settings.MinimizeWhenStarted) - Minimize(); + if (Global.Settings.MinimizeWhenStarted) + Minimize(); - // 自动检测延迟 - async Task StartedPingAsync() + // 自动检测延迟 - NOW WITH PROPER CANCELLATION + async Task StartedPingAsync() + { + try { - while (State == State.Started) + while (State == State.Started && !cancellationToken.IsCancellationRequested) { if (Global.Settings.StartedPingInterval >= 0) { await server.PingAsync(); ServerComboBox.Refresh(); - await Task.Delay(Global.Settings.StartedPingInterval * 1000); + await Task.Delay(Global.Settings.StartedPingInterval * 1000, cancellationToken); } else { - await Task.Delay(5000); + await Task.Delay(5000, cancellationToken); } } } - - StartedPingAsync().Forget(); + catch (OperationCanceledException) + { + // Expected when shutting down + } } + StartedPingAsync().Forget(); +} + #endregion #region SettingsButton @@ -1042,13 +1054,36 @@ public async Task StopAsync() } private async Task StopCoreAsync() +{ + State = State.Stopping; + + // Cancel all background operations + _mainCancellationTokenSource?.Cancel(); + _discoveryNatCts?.Cancel(); + _httpConnectCts?.Cancel(); + + try { - State = State.Stopping; - _discoveryNatCts?.Cancel(); - _httpConnectCts?.Cancel(); - await MainController.StopAsync(); + // Add timeout to prevent infinite hang + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await MainController.StopAsync().WaitAsync(timeoutCts.Token); + } + catch (OperationCanceledException) + { + // Force stop if timeout reached + Log.Warning("MainController.StopAsync() timed out, forcing termination"); + } + catch (Exception ex) + { + Log.Error(ex, "Error during MainController.StopAsync()"); + } + finally + { + _mainCancellationTokenSource?.Dispose(); + _mainCancellationTokenSource = null; State = State.Stopped; } +} private bool IsWaiting() => IsWaiting(_state); @@ -1274,31 +1309,39 @@ private void Minimize() } public async void Exit(bool forceExit = false, bool saveConfiguration = true) +{ + if (!IsWaiting() && !Global.Settings.StopWhenExited && !forceExit) { - if (!IsWaiting() && !Global.Settings.StopWhenExited && !forceExit) - { - MessageBoxX.Show(i18N.Translate("Please press Stop button first")); - - ShowMainFormToolStripButton.PerformClick(); - return; - } + MessageBoxX.Show(i18N.Translate("Please press Stop button first")); + ShowMainFormToolStripButton.PerformClick(); + return; + } - // State = State.Terminating; - NotifyIcon.Visible = false; - Hide(); + // State = State.Terminating; + NotifyIcon.Visible = false; + Hide(); - if (saveConfiguration) - await Configuration.SaveAsync(); - - foreach (var file in new[] { Constants.TempConfig, Constants.TempRouteFile }) - if (File.Exists(file)) - File.Delete(file); + if (saveConfiguration) + await Configuration.SaveAsync(); - await StopAsync(); + foreach (var file in new[] { Constants.TempConfig, Constants.TempRouteFile }) + if (File.Exists(file)) + File.Delete(file); - Dispose(); - Environment.Exit(Environment.ExitCode); + // ADD TIMEOUT HERE TOO + try + { + using var exitTimeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await StopAsync().WaitAsync(exitTimeoutCts.Token); } + catch (OperationCanceledException) + { + Log.Warning("StopAsync() timed out during exit, forcing termination"); + } + + Dispose(); + Environment.Exit(Environment.ExitCode); +} #region FormClosingButton diff --git a/Netch/Interops/RouteHelper.cs b/Netch/Interops/RouteHelper.cs index f8d5fa4087..6741055942 100644 --- a/Netch/Interops/RouteHelper.cs +++ b/Netch/Interops/RouteHelper.cs @@ -18,86 +18,117 @@ public static unsafe class RouteHelper [DllImport("RouteHelper.bin", CallingConvention = CallingConvention.Cdecl)] public static extern bool CreateUnicastIP(AddressFamily inet, string address, byte cidr, ulong index); - public static bool CreateUnicastIPCS(AddressFamily inet, string address, byte cidr, ulong index) - { - MIB_UNICASTIPADDRESS_ROW addr; - InitializeUnicastIpAddressEntry(&addr); + // Replace the CreateUnicastIPCS method in RouteHelper.cs +public static bool CreateUnicastIPCS(AddressFamily inet, string address, byte cidr, ulong index) +{ + MIB_UNICASTIPADDRESS_ROW addr; + InitializeUnicastIpAddressEntry(&addr); - addr.InterfaceIndex = (uint)index; - addr.OnLinkPrefixLength = cidr; + addr.InterfaceIndex = (uint)index; + addr.OnLinkPrefixLength = cidr; - if (inet == AddressFamily.InterNetwork) + if (inet == AddressFamily.InterNetwork) + { + addr.Address.Ipv4.sin_family = (ushort)ADDRESS_FAMILY.AF_INET; + + // Platform guard for inet_pton + if (OperatingSystem.IsWindowsVersionAtLeast(8, 1)) { - addr.Address.Ipv4.sin_family = (ushort)ADDRESS_FAMILY.AF_INET; if (inet_pton((int)inet, address, &addr.Address.Ipv4.sin_addr) == 0) return false; } - else if (inet == AddressFamily.InterNetworkV6) + else + { + // Fallback for Windows 7 + if (!System.Net.IPAddress.TryParse(address, out var ip) || ip.AddressFamily != AddressFamily.InterNetwork) + return false; + var bytes = ip.GetAddressBytes(); + fixed (byte* ptr = bytes) + { + addr.Address.Ipv4.sin_addr = *(IN_ADDR*)ptr; + } + } + } + else if (inet == AddressFamily.InterNetworkV6) + { + addr.Address.Ipv6.sin6_family = (ushort)ADDRESS_FAMILY.AF_INET6; + + // Platform guard for inet_pton + if (OperatingSystem.IsWindowsVersionAtLeast(8, 1)) { - addr.Address.Ipv6.sin6_family = (ushort)ADDRESS_FAMILY.AF_INET6; if (inet_pton((int)inet, address, &addr.Address.Ipv6.sin6_addr) == 0) return false; } else { - return false; + // Fallback for Windows 7 + if (!System.Net.IPAddress.TryParse(address, out var ip) || ip.AddressFamily != AddressFamily.InterNetworkV6) + return false; + var bytes = ip.GetAddressBytes(); + fixed (byte* ptr = bytes) + { + addr.Address.Ipv6.sin6_addr = *(IN6_ADDR*)ptr; + } } + } + else + { + return false; + } - // https://docs.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-createunicastipaddressentry#remarks - - HANDLE handle = default; - using var obj = new Semaphore(0, 1); + // Rest of the method remains the same... + HANDLE handle = default; + using var obj = new Semaphore(0, 1); - void Callback(void* context, MIB_UNICASTIPADDRESS_ROW* row, MIB_NOTIFICATION_TYPE type) + void Callback(void* context, MIB_UNICASTIPADDRESS_ROW* row, MIB_NOTIFICATION_TYPE type) + { + if (type != MIB_NOTIFICATION_TYPE.MibInitialNotification) { - if (type != MIB_NOTIFICATION_TYPE.MibInitialNotification) + NTSTATUS state; + if ((state = GetUnicastIpAddressEntry(row)) != 0) { - NTSTATUS state; - if ((state = GetUnicastIpAddressEntry(row)) != 0) + Log.Error("GetUnicastIpAddressEntry failed: {State}", state.Value); + return; + } + + if (row -> DadState == NL_DAD_STATE.IpDadStatePreferred) + { + try { - Log.Error("GetUnicastIpAddressEntry failed: {State}", state.Value); - return; + obj.Release(); } - - if (row -> DadState == NL_DAD_STATE.IpDadStatePreferred) + catch (Exception e) { - try - { - obj.Release(); - } - catch (Exception e) - { - // i don't trust win32 api - Log.Error(e, "semaphore disposed"); - } + Log.Error(e, "semaphore disposed"); } } } + } - NotifyUnicastIpAddressChange((ushort)ADDRESS_FAMILY.AF_INET, Callback, null, new BOOLEAN(byte.MaxValue), ref handle); + NotifyUnicastIpAddressChange((ushort)ADDRESS_FAMILY.AF_INET, Callback, null, new BOOLEAN(byte.MaxValue), ref handle); - try + try + { + NTSTATUS state; + if ((state = CreateUnicastIpAddressEntry(&addr)) != 0) { - NTSTATUS state; - if ((state = CreateUnicastIpAddressEntry(&addr)) != 0) - { - Log.Error("CreateUnicastIpAddressEntry failed: {State}", state.Value); - return false; - } - - if (!obj.WaitOne(TimeSpan.FromSeconds(10))) - { - Log.Error("Wait unicast IP usable timeout"); - return false; - } - - return true; + Log.Error("CreateUnicastIpAddressEntry failed: {State}", state.Value); + return false; } - finally + + if (!obj.WaitOne(TimeSpan.FromSeconds(10))) { - CancelMibChangeNotify2(handle); + Log.Error("Wait unicast IP usable timeout"); + return false; } + + return true; + } + finally + { + CancelMibChangeNotify2(handle); } +} [DllImport("RouteHelper.bin", CallingConvention = CallingConvention.Cdecl)] public static extern bool RefreshIPTable(AddressFamily inet, ulong index); diff --git a/Netch/Utils/Bandwidth.cs b/Netch/Utils/Bandwidth.cs index c23a5cd0c2..b4a5f38277 100644 --- a/Netch/Utils/Bandwidth.cs +++ b/Netch/Utils/Bandwidth.cs @@ -40,49 +40,55 @@ public static string Compute(ulong d) /// /// 根据程序名统计流量 /// - public static void NetTraffic() - { - if (!Flags.IsWindows10Upper) - return; + // Replace the existing NetTraffic method in Bandwidth.cs +/// +/// 根据程序名统计流量 +/// +public static void NetTraffic(CancellationToken cancellationToken) +{ + if (!Flags.IsWindows10Upper) + return; - var counterLock = new object(); - //int sent = 0; + var counterLock = new object(); + //int sent = 0; - var processes = new List(); - switch (MainController.ServerController) + var processes = new List(); + switch (MainController.ServerController) + { + case null: + break; + case Guard guard: + processes.Add(guard.Instance); + break; + } + + if (!processes.Any()) + switch (MainController.ModeController) { case null: break; + case NFController or TUNController: + processes.Add(Process.GetCurrentProcess()); + break; case Guard guard: processes.Add(guard.Instance); break; } - if (!processes.Any()) - switch (MainController.ModeController) - { - case null: - break; - case NFController or TUNController: - processes.Add(Process.GetCurrentProcess()); - break; - case Guard guard: - processes.Add(guard.Instance); - break; - } - - var pidHastSet = processes.Select(instance => instance.Id).ToHashSet(); + var pidHastSet = processes.Select(instance => instance.Id).ToHashSet(); - Log.Information("Net traffic processes: {Processes}", string.Join(',', processes.Select(v => $"({v.Id}){v.ProcessName}"))); + Log.Information("Net traffic processes: {Processes}", string.Join(',', processes.Select(v => $"({v.Id}){v.ProcessName}"))); - received = 0; + received = 0; - if (!processes.Any()) - return; + if (!processes.Any()) + return; - Global.MainForm.BandwidthState(true); + Global.MainForm.BandwidthState(true); - Task.Run(() => + Task.Run(() => + { + try { tSession = new TraceEventSession("KernelAndClrEventsSession"); tSession.EnableKernelProvider(KernelTraceEventParser.Keywords.NetworkTCPIP); @@ -108,16 +114,45 @@ public static void NetTraffic() }; tSession.Source.Process(); - }) - .Forget(); + } + catch (Exception ex) + { + Log.Error(ex, "Error in traffic monitoring"); + } + }, cancellationToken) + .Forget(); - while (Global.MainForm.State != State.Stopped) + try + { + // FIXED: Use cancellation token instead of State check + while (!cancellationToken.IsCancellationRequested && Global.MainForm.State != State.Stopped) { - Thread.Sleep(1000); + // Use cancellable delay instead of Thread.Sleep + try + { + cancellationToken.WaitHandle.WaitOne(1000); + if (cancellationToken.IsCancellationRequested) + break; + } + catch (OperationCanceledException) + { + break; + } + lock (counterLock) Global.MainForm.OnBandwidthUpdated(received); } } + catch (OperationCanceledException) + { + // Expected when cancellation is requested + } + finally + { + // Ensure cleanup happens + Stop(); + } +} public static void Stop() { diff --git a/Netch/Utils/PortHelper.cs b/Netch/Utils/PortHelper.cs index a77f2cf031..d42e5ee99c 100644 --- a/Netch/Utils/PortHelper.cs +++ b/Netch/Utils/PortHelper.cs @@ -56,7 +56,22 @@ internal static IEnumerable GetProcessByUsedTcpPort(ushort port, Addres if (row.dwOwningPid is 0 or 4) continue; - if (PInvoke.ntohs((ushort)row.dwLocalPort) == port) + // Replace the problematic line in GetProcessByUsedTcpPort method: + // OLD: if (PInvoke.ntohs((ushort)row.dwLocalPort) == port) + // NEW: + ushort localPort; + if (OperatingSystem.IsWindowsVersionAtLeast(8, 1)) + { + localPort = PInvoke.ntohs((ushort)row.dwLocalPort); + } + else + { + // Manual byte swap for older Windows versions + var portBytes = (ushort)row.dwLocalPort; + localPort = (ushort)((portBytes << 8) | (portBytes >> 8)); + } + + if (localPort == port) process.Add(Process.GetProcessById((int)row.dwOwningPid)); } } diff --git a/Other/pcap2socks/pcap2socks.zip b/Other/pcap2socks/pcap2socks.zip new file mode 100644 index 0000000000..87ee723e6c Binary files /dev/null and b/Other/pcap2socks/pcap2socks.zip differ diff --git a/Other/pcap2socks/pcap2socks/LICENSE b/Other/pcap2socks/pcap2socks/LICENSE new file mode 100644 index 0000000000..4e79fbb343 --- /dev/null +++ b/Other/pcap2socks/pcap2socks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Xie Zhihao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Other/pcap2socks/pcap2socks/README.md b/Other/pcap2socks/pcap2socks/README.md new file mode 100644 index 0000000000..8ea35af7fa --- /dev/null +++ b/Other/pcap2socks/pcap2socks/README.md @@ -0,0 +1,95 @@ +# pcap2socks + +**pcap2socks** is a proxy which redirect traffic to a SOCKS proxy with pcap written in Rust. + +You can use [pcap2socks GUI](https://github.com/zhxie/pcap2socks-gui) for a front-end interface. + +_pcap2socks is designed to accelerate games in game consoles._ + +## Features + +- **Redirect Traffic**: Redirect TCP and UDP traffic to a SOCKS proxy. +- **Proxy ARP**: Reply ARP request as it owns the specified address which is not on the network. +- **Cross Platform** +- **Full Cone NAT** + +## Dependencies + +1. [Npcap](http://www.npcap.org/) or WinPcap in Windows (If using Npcap, make sure to install with the "Install Npcap in WinPcap API-compatible Mode"), libpcap in macOS, Linux and others. + +## Build + +### Windows + +If you want to build pcap2socks in Windows, you must meet all the three requirements described in [libpnet](https://github.com/libpnet/libpnet#windows). + +## Usage + +``` +pcap2socks -s
+ +# Or a more general one using proxy ARP (recommended) +pcap2socks -s
-p
-d
+``` + +### Flags + +`-h, --help`: Prints help information. + +`-v, --verbose`: Prints verbose information (`-vv` for vverbose). + +`-V, --version`: Prints version information. + +`--force-associate-destination`, `--force-associate-bind-address`: Force to associate with the destination/replied bind address. pcap2socks will associate with the destination instead of the replied bind address in UDP ASSOCIATE if the replied bind address is in the private network by default. If this flag is set, pcap2socks will force to associate with the destination/replied bind address. If both flags are set, the `--force-associate-destination` will take effect. + +### Options + +`-i, --interface `: Interface for listening. + +`--mtu `: MTU. Generally, pcap2socks will automatically obtain the MTU, but you can also override by setting this option. The MTU is set in the traffic from local to the source. + +`-P, --preset `: Preset. You can use preset source and publish of game accelerators in the market. Available values are `t`, `tencent` for [Tencent Online Game Accelerator](https://jiasu.qq.com/) and `n`, `netease`, `u`, `uu` for [Netease UU Game Accelerator](https://uu.163.com/). + +`-s, --source
`: Source. The source can be a single IPv4 address like `192.168.1.2`, or an IPv4 CIDR network like `10.10.0.1/24`. + +`-p, --publish
`: ARP publishing address. If this option is set, pcap2socks will reply ARP request as it owns the specified address which is not on the network, also called proxy ARP. + +`-d, --destination
`: Destination, default as `127.0.0.1:1080`. + +`--username `: Username. This value should be set only when the SOCKS5 server requires the username/password authentication. + +`--password `: Password. This value should be set only when the SOCKS5 server requires the username/password authentication. + +## Troubleshoot + +1. Because the packet sent from sources should only be handled by pcap2socks, you have to disable IP forward or configure the firewall with the following command statement. For more information, please refer to the troubleshoot paragraph in [IkaGo](https://github.com/zhxie/ikago#troubleshoot). + + ``` + // Linux + sysctl -w net.ipv4.ip_forward=0 + + // macOS + sysctl -w net.inet.ip.forwarding=0 + ``` + +2. pcap2socks requires root permission in some OS by default. But you can run pcap2socks in non-root by executing the following command before opening pcap2socks. + ``` + // Linux + setcap cap_net_raw+ep path_to_pcap2socks + ``` + +## Limitations + +1. IPv6 is not supported yet. + +2. Because only SOCKS5 can forward UDP traffic, pcap2socks only support SOCKS5 at this point. A version with SOCKS4 support without redirecting UDP traffic will release in the future. + +## Known Issues + +1. Applications like VMWare Workstation on Windows may implement their own IP forwarding and forward packets which should be handled by pcap2socks, resulting in abnormal operations in pcap2socks. + +2. The traffic flow control was not implemented in pcap2socks currently. On certain occasions, bandwidth may be heavily occupied by one side of a connection, causing the other side of the connection and other connections to be unable to transmit data normally. + +## License + +pcap2socks is licensed under [the MIT License](/LICENSE). diff --git a/Other/pcap2socks/pcap2socks/pcap2socks.exe b/Other/pcap2socks/pcap2socks/pcap2socks.exe new file mode 100644 index 0000000000..0cd36997c7 Binary files /dev/null and b/Other/pcap2socks/pcap2socks/pcap2socks.exe differ