feat(send-through): 支持配置本地出站地址

为 Xray 增加 SendThrough 配置项,允许指定本机 IPv4 作为出站源地址。

- 在核心设置页新增 SendThrough 输入框及中英文提示文案
- 保存配置时校验并持久化本机 IPv4 地址
- 生成 Xray 配置时为所有 outbound 写入 sendThrough 字段

影响说明:
- 仅对 Xray 生效,留空时不设置该字段
This commit is contained in:
wuzhou 2026-03-16 15:07:56 +08:00
parent 53041906b3
commit bb5563b02e
13 changed files with 153 additions and 2 deletions

View file

@ -522,6 +522,23 @@ public class Utils
return false;
}
public static bool IsIpv4(string? ip)
{
if (ip.IsNullOrEmpty())
{
return false;
}
ip = ip.Trim();
if (!IPAddress.TryParse(ip, out var address))
{
return false;
}
return address.AddressFamily == AddressFamily.InterNetwork
&& ip.Count(c => c == '.') == 3;
}
public static bool IsIpAddress(string? ip)
{
if (ip.IsNullOrEmpty())

View file

@ -41,6 +41,7 @@ public static class ConfigHandler
Loglevel = "warning",
MuxEnabled = false,
};
config.CoreBasicItem.SendThrough = config.CoreBasicItem.SendThrough?.TrimEx();
if (config.Inbound == null)
{

View file

@ -15,6 +15,8 @@ public class CoreBasicItem
public string DefUserAgent { get; set; }
public string? SendThrough { get; set; }
public bool EnableFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;

View file

@ -105,6 +105,8 @@ public class Outbounds4Ray
public string protocol { get; set; }
public string? sendThrough { get; set; }
public string? targetStrategy { get; set; }
public Outboundsettings4Ray settings { get; set; }

View file

@ -222,6 +222,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
/// </summary>
public static string FillCorrectSendThroughIPv4 {
get {
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the correct port format. 的本地化字符串。
/// </summary>
@ -4005,6 +4014,33 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 e.g. 192.168.1.10 的本地化字符串。
/// </summary>
public static string TbSettingsSendThroughHint {
get {
return ResourceManager.GetString("TbSettingsSendThroughHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 Local outbound address (SendThrough) 的本地化字符串。
/// </summary>
public static string TbSettingsSendThrough {
get {
return ResourceManager.GetString("TbSettingsSendThrough", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only applies to Xray. Fill in a local IPv4 address; leave empty to disable. 的本地化字符串。
/// </summary>
public static string TbSettingsSendThroughTip {
get {
return ResourceManager.GetString("TbSettingsSendThroughTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable Log 的本地化字符串。
/// </summary>

View file

@ -1320,6 +1320,18 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart.</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>Local outbound address (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughHint" xml:space="preserve">
<value>e.g. 192.168.1.10</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>Only applies to Xray. Fill in a local IPv4 address; leave empty to disable.</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>Please fill in the correct IPv4 address for SendThrough.</value>
</data>
<data name="TransportHeaderTypeTip5" xml:space="preserve">
<value>*xhttp mode</value>
</data>
@ -1698,4 +1710,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
</data>
</root>
</root>

View file

@ -1317,6 +1317,18 @@
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
<value>密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。</value>
</data>
<data name="TbSettingsSendThrough" xml:space="preserve">
<value>本地出站地址 (SendThrough)</value>
</data>
<data name="TbSettingsSendThroughHint" xml:space="preserve">
<value>例如 192.168.1.10</value>
</data>
<data name="TbSettingsSendThroughTip" xml:space="preserve">
<value>仅对 Xray 生效,填写本机 IPv4留空则不设置。</value>
</data>
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
<value>请填写正确的 SendThrough IPv4 地址。</value>
</data>
<data name="TransportHeaderTypeTip5" xml:space="preserve">
<value>*XHTTP 模式</value>
</data>
@ -1695,4 +1707,4 @@
<data name="TbLegacyProtect" xml:space="preserve">
<value>旧版 TUN 保护</value>
</data>
</root>
</root>

View file

@ -62,6 +62,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenDns();
GenStatistic();
ApplyOutboundSendThrough();
var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag))
@ -195,6 +196,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
_coreConfig.routing.rules.Add(rule);
}
ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@ -255,6 +257,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
});
_coreConfig.routing.rules.Add(BuildFinalRule());
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
@ -376,6 +379,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
//_coreConfig.inbounds.Clear();
ApplyOutboundSendThrough();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new
{
@ -405,4 +409,13 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
}
#endregion public gen function
private void ApplyOutboundSendThrough()
{
var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
foreach (var outbound in _coreConfig.outbounds ?? [])
{
outbound.sendThrough = sendThrough.IsNullOrEmpty() ? null : sendThrough;
}
}
}

View file

@ -20,6 +20,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool defAllowInsecure { get; set; }
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string sendThrough { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
@ -154,6 +155,7 @@ public class OptionSettingViewModel : MyReactiveObject
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
sendThrough = _config.CoreBasicItem.SendThrough;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
@ -297,6 +299,12 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
var sendThroughValue = sendThrough?.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
return;
}
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort
@ -336,6 +344,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
_config.CoreBasicItem.SendThrough = sendThrough?.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;

View file

@ -325,6 +325,27 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="21"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
<TextBox
x:Name="txtsendThrough"
Grid.Row="21"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
Watermark="{x:Static resx:ResUI.TbSettingsSendThroughHint}" />
<TextBlock
Grid.Row="21"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSendThroughTip}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>

View file

@ -76,6 +76,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);

View file

@ -391,6 +391,30 @@
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="21"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
<TextBox
x:Name="txtsendThrough"
Grid.Row="21"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefTextBox}"
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.TbSettingsSendThroughHint}" />
<TextBlock
Grid.Row="21"
Grid.Column="2"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsSendThroughTip}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>

View file

@ -73,6 +73,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);