sending connection data to 3x-ui

This commit is contained in:
shatinz 2026-01-29 15:57:31 +04:00
parent 9ea80671d3
commit 42263c6c2f
15 changed files with 279 additions and 3 deletions

1
3x-ui Submodule

@ -0,0 +1 @@
Subproject commit 70b365171f9b40b540be5bff1a9c8b7227a37ac6

View file

@ -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";

View file

@ -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())

View file

@ -1,5 +1,7 @@
namespace ServiceLib.Manager;
using ServiceLib.Services;
/// <summary>
/// Core process processing class
/// </summary>
@ -14,11 +16,13 @@ public class CoreManager
private bool _linuxSudo = false;
private Func<bool, string, Task>? _updateFunc;
private const string _tag = "CoreHandler";
private ConnectionProbeService _connectionProbeService;
public async Task Init(Config config, Func<bool, string, Task> 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);
});
}
}

View file

@ -34,6 +34,13 @@ public class Config
public List<KeyEventItem> GlobalHotkeys { get; set; }
public List<CoreTypeItem> 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; } = "";
}

View file

@ -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]

View file

@ -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<ReportResult> ProbeAndReportAsync(ProfileItem profile, Func<bool, string, Task> 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; }
}

View file

@ -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
@ -202,6 +203,9 @@ public class OptionSettingViewModel : MyReactiveObject
RoutingRulesSourceUrl = _config.ConstItem.RouteRulesTemplateSourceUrl;
IPAPIUrl = _config.SpeedTestItem.IPAPIUrl;
if (_config.ReportItem == null) _config.ReportItem = new ReportItem();
ReportUrl = _config.ReportItem.ReportUrl;
#endregion UI
#region System proxy
@ -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;

View file

@ -18,7 +18,7 @@ public partial class App : Application
/// Open only one process
/// </summary>
/// <param name="e"></param>
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);
}

View file

@ -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"

View file

@ -522,6 +522,33 @@
</Grid>
</TabItem>-->
<TabItem Header="Reporting">
<Grid Margin="{StaticResource Margin8}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="Report URL" />
<TextBox
x:Name="txtReportUrl"
Grid.Row="0"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefTextBox}" />
</Grid>
</TabItem>
<TabItem Header="{x:Static resx:ResUI.TbSettingsN}">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid Grid.Row="2" Margin="{StaticResource Margin8}">

View file

@ -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);

View file

@ -0,0 +1,25 @@
<Window x:Class="v2rayN.Views.PrivacyNoticeWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:v2rayN.Views"
mc:Ignorable="d"
Title="Privacy Notice" Height="250" Width="400"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="True"
Topmost="True">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center" TextAlignment="Center">
Note that we use connection data only for improving the services and we do not take the private information.
</TextBlock>
<Button Grid.Row="1" x:Name="btnAccept" Content="Accept" Width="100" HorizontalAlignment="Center" Margin="0,20,0,0" Click="btnAccept_Click"/>
</Grid>
</Window>

View file

@ -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();
}
}
}

View file

@ -8,6 +8,9 @@
<ApplicationIcon>Resources\v2rayN.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AssemblyTitle>shi2ray</AssemblyTitle>
<Product>shi2ray</Product>
<Title>shi2ray</Title>
</PropertyGroup>
<ItemGroup>