diff --git a/3x-ui b/3x-ui
new file mode 160000
index 00000000..70b36517
--- /dev/null
+++ b/3x-ui
@@ -0,0 +1 @@
+Subproject commit 70b365171f9b40b540be5bff1a9c8b7227a37ac6
diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs
index 94a66fa5..3feb753b 100644
--- a/v2rayN/ServiceLib/Global.cs
+++ b/v2rayN/ServiceLib/Global.cs
@@ -4,7 +4,7 @@ public class Global
{
#region const
- public const string AppName = "v2rayN";
+ public const string AppName = "shi2ray";
public const string GithubUrl = "https://github.com";
public const string GithubApiUrl = "https://api.github.com/repos";
public const string GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/{0}.dat";
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 5877d012..1109ee51 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -1334,6 +1334,30 @@ public static class ConfigHandler
return -1;
}
+ // Check for reportUrl and auto-set
+ if (strData.Contains("reportUrl="))
+ {
+ try
+ {
+ // Simple regex or string parsing to find reportUrl value
+ // It might be URL encoded, but usually it's just a query param.
+ // Regex pattern to capture reportUrl value until next & or end of line/string
+ var match = System.Text.RegularExpressions.Regex.Match(strData, @"reportUrl=([^&\s]+)");
+ if (match.Success)
+ {
+ var url = match.Groups[1].Value;
+ if (!string.IsNullOrEmpty(url))
+ {
+ // Decode if necessary (it might be double encoded if inside a URI)
+ // But for now take simpler approach
+ config.ReportItem.ReportUrl = System.Web.HttpUtility.UrlDecode(url);
+ }
+ }
+ }
+ catch {}
+ }
+
+
var subFilter = string.Empty;
//remove sub items
if (isSub && subid.IsNotEmpty())
diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs
index 79dbdeb0..b372ee2c 100644
--- a/v2rayN/ServiceLib/Manager/CoreManager.cs
+++ b/v2rayN/ServiceLib/Manager/CoreManager.cs
@@ -1,5 +1,7 @@
namespace ServiceLib.Manager;
+using ServiceLib.Services;
+
///
/// Core process processing class
///
@@ -14,11 +16,13 @@ public class CoreManager
private bool _linuxSudo = false;
private Func? _updateFunc;
private const string _tag = "CoreHandler";
+ private ConnectionProbeService _connectionProbeService;
public async Task Init(Config config, Func updateFunc)
{
_config = config;
_updateFunc = updateFunc;
+ _connectionProbeService = new ConnectionProbeService(config);
//Copy the bin folder to the storage location (for init)
if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")
@@ -90,6 +94,12 @@ public class CoreManager
if (_processService != null)
{
await UpdateFunc(true, $"{node.GetSummary()}");
+
+ // Auto Report
+ _ = Task.Run(async () => {
+ await Task.Delay(2000); // Wait for core to stabilize
+ await _connectionProbeService.ProbeAndReportAsync(node, _updateFunc);
+ });
}
}
diff --git a/v2rayN/ServiceLib/Models/Config.cs b/v2rayN/ServiceLib/Models/Config.cs
index 738ee286..6459e2fb 100644
--- a/v2rayN/ServiceLib/Models/Config.cs
+++ b/v2rayN/ServiceLib/Models/Config.cs
@@ -34,6 +34,13 @@ public class Config
public List GlobalHotkeys { get; set; }
public List CoreTypeItem { get; set; }
public SimpleDNSItem SimpleDNSItem { get; set; }
+ public ReportItem ReportItem { get; set; }
#endregion other entities
}
+
+[Serializable]
+public class ReportItem
+{
+ public string ReportUrl { get; set; } = "";
+}
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index eeb88deb..252bcd34 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -74,6 +74,7 @@ public class GUIItem
public int TrayMenuServersLimit { get; set; } = 20;
public bool EnableHWA { get; set; } = false;
public bool EnableLog { get; set; } = true;
+ public bool PrivacyNoticeAccepted { get; set; }
}
[Serializable]
diff --git a/v2rayN/ServiceLib/Services/ConnectionProbeService.cs b/v2rayN/ServiceLib/Services/ConnectionProbeService.cs
new file mode 100644
index 00000000..7f15a75a
--- /dev/null
+++ b/v2rayN/ServiceLib/Services/ConnectionProbeService.cs
@@ -0,0 +1,141 @@
+using System.Net.NetworkInformation;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text;
+
+namespace ServiceLib.Services;
+
+public class ConnectionProbeService
+{
+ private static readonly string _tag = "ConnectionProbeService";
+ private readonly Config _config;
+
+ public ConnectionProbeService(Config config)
+ {
+ _config = config;
+ }
+
+ public async Task ProbeAndReportAsync(ProfileItem profile, Func updateFunc = null)
+ {
+ var result = new ReportResult();
+
+ if (updateFunc != null) await updateFunc(false, "Starting Connection Probe & Report...");
+
+ // 1. Gather System Info
+ result.SystemInfo = GetSystemInfo();
+
+ // 2. Test Connection (Ping)
+ // Using existing ConnectionHandler logic but simplified for single target
+ try
+ {
+ var webProxy = new WebProxy($"socks5://{Global.Loopback}:{profile.Port}");
+ var url = _config.SpeedTestItem.SpeedPingTestUrl;
+ var responseTime = await ConnectionHandler.GetRealPingTime(url, webProxy, 10);
+
+ result.ConnectionQuality = new ConnectionQuality
+ {
+ Latency = responseTime,
+ Success = responseTime > 0
+ };
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ result.ConnectionQuality = new ConnectionQuality { Success = false, Message = ex.Message };
+ }
+
+ // 3. Populate Protocol Info
+ result.ProtocolInfo = new ProtocolInfo
+ {
+ Protocol = profile.ConfigType.ToString(),
+ Remarks = profile.Remarks,
+ Address = profile.Address
+ };
+
+ // 4. Send Report
+ if (!string.IsNullOrEmpty(_config.ReportItem.ReportUrl))
+ {
+ if (updateFunc != null) await updateFunc(false, "Sending Connection Report...");
+ await SendReportAsync(result);
+ if (updateFunc != null) await updateFunc(false, "Connection Report Sent Successfully.");
+ }
+
+ return result;
+ }
+
+ private SystemInfo GetSystemInfo()
+ {
+ var info = new SystemInfo();
+ try
+ {
+ // Simple heuristic to guess active interface
+ var interfaces = NetworkInterface.GetAllNetworkInterfaces()
+ .Where(n => n.OperationalStatus == OperationalStatus.Up)
+ .Where(n => n.NetworkInterfaceType != NetworkInterfaceType.Loopback)
+ .ToList();
+
+ if (interfaces.Count > 0)
+ {
+ // Prioritize Ethernet/Wifi
+ var bestMatch = interfaces.FirstOrDefault(n => n.NetworkInterfaceType == NetworkInterfaceType.Ethernet || n.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
+ ?? interfaces.First();
+
+ info.InterfaceName = bestMatch.Name;
+ info.InterfaceDescription = bestMatch.Description;
+ info.InterfaceType = bestMatch.NetworkInterfaceType.ToString();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ info.Message = "Failed to gather system info";
+ }
+ return info;
+ }
+
+ private async Task SendReportAsync(ReportResult report)
+ {
+ try
+ {
+ using var client = new HttpClient();
+ var json = JsonSerializer.Serialize(report);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var response = await client.PostAsync(_config.ReportItem.ReportUrl, content);
+ response.EnsureSuccessStatusCode();
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ throw; // Re-throw to show error in UI
+ }
+ }
+}
+
+public class ReportResult
+{
+ public SystemInfo SystemInfo { get; set; }
+ public ConnectionQuality ConnectionQuality { get; set; }
+ public ProtocolInfo ProtocolInfo { get; set; }
+}
+
+public class SystemInfo
+{
+ public string InterfaceName { get; set; }
+ public string InterfaceDescription { get; set; } // Often contains SIM/ISP info (e.g., "Intel Wi-Fi", "Quectel Mobile Broadband")
+ public string InterfaceType { get; set; }
+ public string Message { get; set; }
+}
+
+public class ConnectionQuality
+{
+ public int Latency { get; set; }
+ public bool Success { get; set; }
+ public string Message { get; set; }
+}
+
+public class ProtocolInfo
+{
+ public string Protocol { get; set; }
+ public string Remarks { get; set; }
+ public string Address { get; set; }
+}
diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
index 86251873..a407c1d0 100644
--- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
@@ -67,6 +67,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string SrsFileSourceUrl { get; set; }
[Reactive] public string RoutingRulesSourceUrl { get; set; }
[Reactive] public string IPAPIUrl { get; set; }
+ [Reactive] public string ReportUrl { get; set; }
#endregion UI
@@ -201,6 +202,9 @@ public class OptionSettingViewModel : MyReactiveObject
SrsFileSourceUrl = _config.ConstItem.SrsSourceUrl;
RoutingRulesSourceUrl = _config.ConstItem.RouteRulesTemplateSourceUrl;
IPAPIUrl = _config.SpeedTestItem.IPAPIUrl;
+
+ if (_config.ReportItem == null) _config.ReportItem = new ReportItem();
+ ReportUrl = _config.ReportItem.ReportUrl;
#endregion UI
@@ -367,6 +371,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.ConstItem.SrsSourceUrl = SrsFileSourceUrl;
_config.ConstItem.RouteRulesTemplateSourceUrl = RoutingRulesSourceUrl;
_config.SpeedTestItem.IPAPIUrl = IPAPIUrl;
+ _config.ReportItem.ReportUrl = ReportUrl;
//systemProxy
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
diff --git a/v2rayN/v2rayN/App.xaml.cs b/v2rayN/v2rayN/App.xaml.cs
index ca56311b..56976a4a 100644
--- a/v2rayN/v2rayN/App.xaml.cs
+++ b/v2rayN/v2rayN/App.xaml.cs
@@ -18,7 +18,7 @@ public partial class App : Application
/// Open only one process
///
///
- protected override void OnStartup(StartupEventArgs e)
+ protected override async void OnStartup(StartupEventArgs e)
{
var exePathKey = Utils.GetMd5(Utils.GetExePath());
@@ -38,6 +38,18 @@ public partial class App : Application
return;
}
+ if (AppManager.Instance.Config.GuiItem.PrivacyNoticeAccepted == false)
+ {
+ var window = new Views.PrivacyNoticeWindow();
+ if (window.ShowDialog() != true)
+ {
+ Environment.Exit(0);
+ return;
+ }
+ AppManager.Instance.Config.GuiItem.PrivacyNoticeAccepted = true;
+ await ConfigHandler.SaveConfig(AppManager.Instance.Config);
+ }
+
AppManager.Instance.InitComponents();
base.OnStartup(e);
}
diff --git a/v2rayN/v2rayN/Views/MainWindow.xaml b/v2rayN/v2rayN/Views/MainWindow.xaml
index 1c49ca24..9ebf36b9 100644
--- a/v2rayN/v2rayN/Views/MainWindow.xaml
+++ b/v2rayN/v2rayN/Views/MainWindow.xaml
@@ -10,7 +10,7 @@
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:view="clr-namespace:v2rayN.Views"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
- Title="v2rayN"
+ Title="shi2ray"
Width="1200"
Height="800"
MinWidth="800"
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
index 00b2dc9e..59f3efdb 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
@@ -522,6 +522,33 @@
-->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
index 8aa0cd02..daec556b 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
@@ -110,6 +110,8 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IPAPIUrl, v => v.cmbIPAPIUrl.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.ReportUrl, v => v.txtReportUrl.Text).DisposeWith(disposables);
+
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
diff --git a/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml b/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml
new file mode 100644
index 00000000..185f767c
--- /dev/null
+++ b/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+ Note that we use connection data only for improving the services and we do not take the private information.
+
+
+
+
+
diff --git a/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml.cs b/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml.cs
new file mode 100644
index 00000000..de88010f
--- /dev/null
+++ b/v2rayN/v2rayN/Views/PrivacyNoticeWindow.xaml.cs
@@ -0,0 +1,18 @@
+using System.Windows;
+
+namespace v2rayN.Views
+{
+ public partial class PrivacyNoticeWindow : Window
+ {
+ public PrivacyNoticeWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void btnAccept_Click(object sender, RoutedEventArgs e)
+ {
+ this.DialogResult = true;
+ this.Close();
+ }
+ }
+}
diff --git a/v2rayN/v2rayN/v2rayN.csproj b/v2rayN/v2rayN/v2rayN.csproj
index 9654625d..01162b47 100644
--- a/v2rayN/v2rayN/v2rayN.csproj
+++ b/v2rayN/v2rayN/v2rayN.csproj
@@ -8,6 +8,9 @@
Resources\v2rayN.ico
app.manifest
7.0
+ shi2ray
+ shi2ray
+ shi2ray