diff --git a/.gitmodules b/.gitmodules index 0ab249d5..75f53f31 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "v2rayN/GlobalHotKeys"] path = v2rayN/GlobalHotKeys url = https://github.com/2dust/GlobalHotKeys +[submodule "v2rayN/NetBridge"] + path = v2rayN/NetBridge + url = https://github.com/2dust/NetBridge diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 57358a75..3956ca9f 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -161,6 +161,10 @@ public static class ConfigHandler config.SystemProxyItem ??= new(); config.WebDavItem ??= new(); config.CheckUpdateItem ??= new(); + config.NetBridgeItem ??= new() + { + RuleProcess = string.Empty + }; config.Fragment4RayItem ??= new() { Packets = "tlshello", diff --git a/v2rayN/ServiceLib/Manager/NetBridgeManager.cs b/v2rayN/ServiceLib/Manager/NetBridgeManager.cs new file mode 100644 index 00000000..3ea21e14 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/NetBridgeManager.cs @@ -0,0 +1,222 @@ +using NetBridgeLib.Services; + +namespace ServiceLib.Manager; + +public sealed class NetBridgeManager +{ + private static readonly Lazy _instance = new(() => new()); + public static NetBridgeManager Instance => _instance.Value; + private readonly Config _config = AppManager.Instance.Config; + private NetBridgeService? _netBridgeService; + private bool _isProxyRunning; + private bool _isInitialized; + private List _ruleConfigs = []; + private Func? _updateFunc; + private uint _proxyConfigId; + + public async Task Init(Func? updateFunc = null) + { + if (_isInitialized) + { + return; + } + + _updateFunc = updateFunc; + + try + { + _netBridgeService = new NetBridgeService(); + _netBridgeService.LogReceived += msg => + { + var message = $"NetBridge Log: {msg}"; + _ = _updateFunc?.Invoke(false, message); + }; + + _netBridgeService.ConnectionReceived += (processName, pid, destIp, destPort, proxyInfo) => + { + var message = $"NetBridge Connection: {processName} (PID: {pid}) -> {destIp}:{destPort} -> {proxyInfo}"; + _ = _updateFunc?.Invoke(false, message); + }; + + _ruleConfigs = BuildRuleConfigs(_config.NetBridgeItem?.RuleProcess); + _isInitialized = true; + } + catch (Exception ex) + { + var error = $"Failed to initialize NetBridgeService: {ex.Message}"; + await _updateFunc?.Invoke(true, error); + } + } + + public async Task Start() + { + if (_isProxyRunning) + { + return true; + } + + try + { + if (_netBridgeService == null) + { + return false; + } + + var started = _netBridgeService.Start(); + if (!started) + { + return false; + } + + _isProxyRunning = true; + } + catch (Exception ex) + { + var error = $"Failed to start NetBridgeService: {ex.Message}"; + await _updateFunc?.Invoke(true, error); + return false; + } + + return true; + } + + public async Task Stop() + { + if (!_isProxyRunning) + { + return true; + } + + try + { + if (_netBridgeService == null) + { + return false; + } + + var stopped = _netBridgeService.Stop(); + if (!stopped) + { + return false; + } + + _isProxyRunning = false; + } + catch (Exception ex) + { + var error = $"Failed to stop NetBridgeService: {ex.Message}"; + await _updateFunc?.Invoke(true, error); + return false; + } + + return true; + } + + public async Task UpdateRoutes(string? ruleProcess) + { + var newRuleConfigs = BuildRuleConfigs(ruleProcess); + + _ruleConfigs = newRuleConfigs; + + if (!_isProxyRunning) + { + return true; + } + + return await ApplyRoutesInternal(); + } + + public async Task UpdateProxyConfig(string proxyHost, int proxyPort) + { + try + { + if (_netBridgeService == null) + { + return false; + } + + var proxyType = "SOCKS5"; + var username = ""; + var password = ""; + + if (_proxyConfigId > 0) + { + var edited = _netBridgeService.EditProxyConfig(_proxyConfigId, proxyType, proxyHost, (ushort)proxyPort, username, password); + if (!edited) + { + return false; + } + } + else + { + _proxyConfigId = _netBridgeService.AddProxyConfig(proxyType, proxyHost, (ushort)proxyPort, username, password); + if (_proxyConfigId == 0) + { + return false; + } + } + + return await Task.FromResult(true); + } + catch (Exception ex) + { + var error = $"Failed to update proxy config: {ex.Message}"; + await _updateFunc?.Invoke(true, error); + return false; + } + } + + private async Task ApplyRoutesInternal() + { + if (_netBridgeService == null) + { + return false; + } + + List rules; + + rules = _ruleConfigs.Select(JsonUtils.DeepCopy).ToList(); + + foreach (var rule in rules.Where(x => x.RuleId > 0)) + { + try + { + _ = _netBridgeService.DeleteRule(rule.RuleId); + } + catch + { + // ignored + } + } + + for (var i = 0; i < rules.Count; i++) + { + var rule = rules[i]; + var newRuleId = _netBridgeService.AddRule(rule.ProcessName, rule.TargetHosts, rule.TargetPorts, rule.Protocol, rule.Action, rule.ProxyConfigId); + if (newRuleId == 0) + { + return false; + } + + rules[i].RuleId = newRuleId; + } + + _ruleConfigs = rules; + + return await Task.FromResult(true); + } + + private static List BuildRuleConfigs(string? ruleProcess) + { + var processNames = Utils.String2List(Utils.Convert2Comma(ruleProcess)); + return processNames.Select(processName => new NetBridgeRuleConfig + { + ProcessName = processName, + TargetHosts = "*", + TargetPorts = "*", + Protocol = "BOTH", + Action = "PROXY", + ProxyConfigId = 0 + }).ToList(); + } +} diff --git a/v2rayN/ServiceLib/Models/Configs/Config.cs b/v2rayN/ServiceLib/Models/Configs/Config.cs index 69770d3c..9ebc2c41 100644 --- a/v2rayN/ServiceLib/Models/Configs/Config.cs +++ b/v2rayN/ServiceLib/Models/Configs/Config.cs @@ -29,6 +29,7 @@ public class Config public SystemProxyItem SystemProxyItem { get; set; } public WebDavItem WebDavItem { get; set; } public CheckUpdateItem CheckUpdateItem { get; set; } + public NetBridgeItem NetBridgeItem { get; set; } public Fragment4RayItem? Fragment4RayItem { get; set; } public List Inbound { get; set; } public List GlobalHotkeys { get; set; } diff --git a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index ea2dcaac..9e18e094 100644 --- a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -277,3 +277,9 @@ public class SimpleDNSItem public string? Hosts { get; set; } public string? DirectExpectedIPs { get; set; } } + +[Serializable] +public class NetBridgeItem +{ + public string? RuleProcess { get; set; } +} diff --git a/v2rayN/ServiceLib/Models/Dto/NetBridgeRuleConfig.cs b/v2rayN/ServiceLib/Models/Dto/NetBridgeRuleConfig.cs new file mode 100644 index 00000000..a12a64ab --- /dev/null +++ b/v2rayN/ServiceLib/Models/Dto/NetBridgeRuleConfig.cs @@ -0,0 +1,12 @@ +namespace ServiceLib.Models.Dto; + +public sealed class NetBridgeRuleConfig +{ + public uint RuleId { get; set; } + public string ProcessName { get; set; } + public string TargetHosts { get; set; } + public string TargetPorts { get; set; } + public string Protocol { get; set; } + public string Action { get; set; } + public uint ProxyConfigId { get; set; } +} diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 68bec1bb..65e6e8e6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -1320,6 +1320,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Process traffic hijacking (experimental) 的本地化字符串。 + /// + public static string menuNetBridge { + get { + return ResourceManager.GetString("menuNetBridge", resourceCulture); + } + } + /// /// 查找类似 New Update 的本地化字符串。 /// @@ -2301,6 +2310,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly. 的本地化字符串。 + /// + public static string NetBridgeRuleTips { + get { + return ResourceManager.GetString("NetBridgeRuleTips", resourceCulture); + } + } + /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -3051,6 +3069,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously; 的本地化字符串。 + /// + public static string TbEnableNetBridge { + get { + return ResourceManager.GetString("TbEnableNetBridge", resourceCulture); + } + } + /// /// 查找类似 Enable Tun 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 0b82e788..0a5182ee 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1770,4 +1770,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Split the tail of packets into smaller fragments. This may affect throughput and latency. - \ No newline at end of file + + Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously; + + + The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly. + + + Process traffic hijacking (experimental) + + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index ada0d389..3eb10116 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1767,4 +1767,13 @@ 将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。 + + 启用进程流量劫持(实验性),类似 Proxifer 功能;可以和系统代理并存;和 TUN 模式冲突,请不要同时开启; + + + 需要代理的进程名,多个进程请用逗号分隔;不在列表中的进程也会被劫持,但是会直连 + + + 进程流量劫持(实验性) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 9a52f0a4..a7facc02 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1767,4 +1767,13 @@ 將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲 - + + 啟用進程流量劫持(實驗性),類似 Proxifer 功能;可以和系統代理並存;和 TUN 模式衝突,請不要同時開啟; + + + 需要代理程式的進程名,多個進程請用逗號分隔;不在清單中的進程也會被劫持,但是會直連 + + + 進程流量劫持(實驗性) + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index d80d1ec8..c5bde1fc 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -87,6 +87,7 @@ + diff --git a/v2rayN/ServiceLib/ViewModels/NetBridgeViewModel.cs b/v2rayN/ServiceLib/ViewModels/NetBridgeViewModel.cs new file mode 100644 index 00000000..43c37cc7 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/NetBridgeViewModel.cs @@ -0,0 +1,100 @@ +namespace ServiceLib.ViewModels; + +public class NetBridgeViewModel : MyReactiveObject +{ + [Reactive] + public bool EnableNetBridge { get; set; } + + [Reactive] + public string RuleProcess { get; set; } + + public ReactiveCommand SaveRulesCmd { get; } + + public NetBridgeViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + SaveRulesCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveRulesAsync(); + }); + + this.WhenAnyValue(x => x.EnableNetBridge) + .Skip(1) + .Subscribe(async enabled => + { + await ToggleNetBridgeAsync(enabled); + }); + + _ = Init(); + } + + private async Task Init() + { + _config.NetBridgeItem ??= new() + { + RuleProcess = string.Empty + }; + + EnableNetBridge = false; + RuleProcess = _config.NetBridgeItem.RuleProcess; + if (RuleProcess.IsNullOrEmpty()) + { + RuleProcess = "Chrome.exe"; + } + + await Task.CompletedTask; + } + + private async Task ToggleNetBridgeAsync(bool enabled) + { + await NetBridgeManager.Instance.Init(UpdateViewHandler); + + if (enabled) + { + var succeed = await NetBridgeManager.Instance.Start(); + if (succeed) + { + await NetBridgeManager.Instance.UpdateProxyConfig(Global.Loopback, AppManager.Instance.GetLocalPort(EInboundProtocol.socks)); + await NetBridgeManager.Instance.UpdateRoutes(RuleProcess); + } + NoticeManager.Instance.Enqueue(succeed ? ResUI.OperationSuccess : ResUI.OperationFailed); + } + else + { + var succeed = await NetBridgeManager.Instance.Stop(); + NoticeManager.Instance.Enqueue(succeed ? ResUI.OperationSuccess : ResUI.OperationFailed); + } + } + + /// + private async Task SaveRulesAsync() + { + _config.NetBridgeItem ??= new(); + + var normalizedRuleProcess = RuleProcess; + _config.NetBridgeItem.RuleProcess = normalizedRuleProcess; + RuleProcess = normalizedRuleProcess; + + if (await ConfigHandler.SaveConfig(_config) != 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + + await NetBridgeManager.Instance.Init(UpdateViewHandler); + if (EnableNetBridge) + { + var routesUpdated = await NetBridgeManager.Instance.UpdateRoutes(normalizedRuleProcess); + NoticeManager.Instance.Enqueue(routesUpdated ? ResUI.OperationSuccess : ResUI.OperationFailed); + } + } + + private async Task UpdateViewHandler(bool isError, string msg) + { + NoticeManager.Instance.SendMessageEx(msg); + + return await Task.FromResult(true); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml index 5473f96f..765b83c5 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml @@ -89,6 +89,7 @@ + diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index 56457590..ba9edac3 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -12,6 +12,7 @@ public partial class MainWindow : WindowBase private readonly WindowNotificationManager? _manager; private CheckUpdateView? _checkUpdateView; private BackupAndRestoreView? _backupAndRestoreView; + private NetBridgeView? _netBridgeView; private bool _blCloseByUser = false; public MainWindow() @@ -28,6 +29,7 @@ public partial class MainWindow : WindowBase btnNewUpdate.Click += MenuCheckUpdate_Click; menuBackupAndRestore.Click += MenuBackupAndRestore_Click; menuClose.Click += MenuClose_Click; + menuNetBridge.Click += MenuNetBridge_Click; ViewModel = new MainWindowViewModel(UpdateViewHandler); @@ -165,6 +167,7 @@ public partial class MainWindow : WindowBase { Title = $"{Utils.GetVersion()}"; menuAddServerViaScan.IsVisible = false; + menuNetBridge.IsVisible = false; } if (_config.UiItem.AutoHideStartup && Utils.IsWindows()) @@ -379,6 +382,19 @@ public partial class MainWindow : WindowBase DialogHost.Show(_backupAndRestoreView); } + private void MenuNetBridge_Click(object? sender, RoutedEventArgs e) + { + if (Utils.IsAdministrator()) + { + _netBridgeView ??= new NetBridgeView(); + DialogHost.Show(_netBridgeView); + } + else + { + NoticeManager.Instance.SendMessageAndEnqueue(ResUI.RunAsAdmin); + } + } + private async void MenuClose_Click(object? sender, RoutedEventArgs e) { if (await UI.ShowYesNo(this, ResUI.menuExitTips) != ButtonResult.Yes) diff --git a/v2rayN/v2rayN.Desktop/Views/NetBridgeView.axaml b/v2rayN/v2rayN.Desktop/Views/NetBridgeView.axaml new file mode 100644 index 00000000..a48e44bc --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/NetBridgeView.axaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + +