diff --git a/Rainbow.Data/Services/DnsmasqManager.cs b/Rainbow.Data/Services/DnsmasqManager.cs
index 710a240..e111680 100644
--- a/Rainbow.Data/Services/DnsmasqManager.cs
+++ b/Rainbow.Data/Services/DnsmasqManager.cs
@@ -36,8 +36,8 @@ public class DnsmasqManager
// 3. 确保配置目录存在
var dir = Path.GetDirectoryName(configPath);
- if (!String.IsNullOrEmpty(dir) && !Directory.Exists(dir))
- await _shell.ExecuteAsync("mkdir", $"-p {dir}", true);
+ if (!String.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
// 4. 委托适配器生成配置、写入文件并重载服务
var applyResult = await _adapter.ApplyConfigAsync(config);
@@ -45,7 +45,7 @@ public class DnsmasqManager
{
// 失败则回滚旧配置
XTrace.WriteLine("[DnsmasqManager] 应用配置失败,尝试回滚");
- await _shell.ExecuteAsync("cp", $"{backupPath} {configPath}", true);
+ try { if (File.Exists(backupPath)) File.Copy(backupPath, configPath, true); } catch (Exception ex) { XTrace.WriteLine($"[DnsmasqManager] 回滚失败: {ex.Message}"); }
return false;
}
@@ -60,11 +60,24 @@ public class DnsmasqManager
}
/// <summary>备份配置文件</summary>
- private async Task BackupConfigAsync(String configPath, String backupPath)
+ private Task BackupConfigAsync(String configPath, String backupPath)
{
- var result = await _shell.ExecuteAsync("cp", $"{configPath} {backupPath}");
- if (!result.Success)
- XTrace.WriteLine($"[DnsmasqManager] 备份配置失败(可能文件不存在): {result.Stderr}");
+ try
+ {
+ if (File.Exists(configPath))
+ {
+ File.Copy(configPath, backupPath, true);
+ }
+ else
+ {
+ XTrace.WriteLine("[DnsmasqManager] 备份配置跳过(源文件不存在)");
+ }
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"[DnsmasqManager] 备份配置失败: {ex.Message}");
+ }
+ return Task.CompletedTask;
}
/// <summary>从 SQLite 数据库加载全量 DHCP/DNS 配置</summary>
diff --git a/Rainbow.Data/Services/FirewallManager.cs b/Rainbow.Data/Services/FirewallManager.cs
index 6abd174..58ba405 100644
--- a/Rainbow.Data/Services/FirewallManager.cs
+++ b/Rainbow.Data/Services/FirewallManager.cs
@@ -17,11 +17,12 @@ public class FirewallManager
public ShellExecutor Shell { get; set; }
public ConfigBackup Backup { get; set; }
public ILog Log { get; set; } = Logger.Null;
+ private readonly IFirewallAdapter? _adapter;
private TimerX _scheduleTimer;
private readonly ConcurrentDictionary<Int32, (String StartTime, String EndTime, Int32 WeekDays)> _schedules = new();
private Int32 _scheduling;
- public FirewallManager(ShellExecutor shell = null, ConfigBackup backup = null) { Shell = shell; Backup = backup; }
+ public FirewallManager(ShellExecutor shell = null, ConfigBackup backup = null, IFirewallAdapter? adapter = null) { Shell = shell; Backup = backup; _adapter = adapter; }
public void SetSchedule(Int32 ruleId, String startTime, String endTime, Int32 weekDays = 127)
{
@@ -64,5 +65,21 @@ public class FirewallManager
public IReadOnlyDictionary<Int32, (String StartTime, String EndTime, Int32 WeekDays)> GetSchedules() => _schedules;
- public async Task<ShellResult> SaveRulesAsync() => ShellResult.Fail("待 Linux 环境实现");
+ public async Task<ShellResult> SaveRulesAsync()
+ {
+ if (_adapter != null)
+ {
+ var ok = await _adapter.SaveAsync();
+ return new ShellResult { ExitCode = ok ? 0 : -1, Stdout = ok ? "防火墙规则已保存" : null, Stderr = ok ? null : "保存防火墙规则失败" };
+ }
+
+ // 无适配器时回退到 shell 命令(Linux iptables-save)
+ if (Shell != null)
+ {
+ var result = await Shell.ExecuteAsync("iptables-save", "", true);
+ return result;
+ }
+
+ return ShellResult.Fail("无可用防火墙适配器");
+ }
}
diff --git a/Rainbow.Data/Services/MonitorCollector.cs b/Rainbow.Data/Services/MonitorCollector.cs
index 3fa0de7..e690fb3 100644
--- a/Rainbow.Data/Services/MonitorCollector.cs
+++ b/Rainbow.Data/Services/MonitorCollector.cs
@@ -4,20 +4,19 @@ namespace Rainbow.Services;
public class MonitorCollector
{
private readonly IOSAdapter _osAdapter;
- private INetworkStatProvider? _networkStatProvider;
+ private readonly INetworkStatProvider? _networkStatProvider;
private Dictionary<String, NetworkStats> _lastStats = new();
private DateTime _lastCollectTime = DateTime.MinValue;
/// <summary>构造监控采集器</summary>
/// <param name="osAdapter">操作系统适配器</param>
- public MonitorCollector(IOSAdapter osAdapter)
+ /// <param name="networkStatProvider">网口流量统计提供者(可选,Windows 上由 DI 注入,Linux 上由 DebianAdapter 提供)</param>
+ public MonitorCollector(IOSAdapter osAdapter, INetworkStatProvider? networkStatProvider = null)
{
_osAdapter = osAdapter ?? throw new ArgumentNullException(nameof(osAdapter));
+ _networkStatProvider = networkStatProvider;
}
- /// <summary>注入网口流量统计提供者(可选,Linux 下由 DebianAdapter 提供)</summary>
- public void SetNetworkStatProvider(INetworkStatProvider provider) => _networkStatProvider = provider;
-
/// <summary>获取 CPU 使用率(委托给 IOSAdapter)</summary>
public Double GetCpuUsage() => _osAdapter.GetCpuUsage();
diff --git a/Rainbow.Data/Services/StatCollector.cs b/Rainbow.Data/Services/StatCollector.cs
index cf2b63b..a94f054 100644
--- a/Rainbow.Data/Services/StatCollector.cs
+++ b/Rainbow.Data/Services/StatCollector.cs
@@ -17,8 +17,16 @@ namespace Rainbow.Services;
public class StatCollector
{
private TimerX? _timer;
+ private readonly INetworkStatProvider? _statProvider;
private readonly ConcurrentDictionary<String, (Int64 rx, Int64 tx, DateTime time)> _lastNetStats = new();
+ /// <summary>构造统计采集器</summary>
+ /// <param name="statProvider">网口流量统计提供者(可选,Windows 上由 DI 注入)</param>
+ public StatCollector(INetworkStatProvider? statProvider = null)
+ {
+ _statProvider = statProvider;
+ }
+
/// <summary>启动采集。每分钟执行一次</summary>
public void Start()
{
@@ -69,10 +77,16 @@ public class StatCollector
#region 网口流量采集
- /// <summary>从 /proc/net/dev 采集网口流量,计算差值写入 InterfaceStat</summary>
+ /// <summary>从系统采集网口流量,计算差值写入 InterfaceStat</summary>
private void CollectInterfaceStat()
{
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return;
+ // Windows 上使用 INetworkStatProvider 采集
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ if (_statProvider != null)
+ CollectInterfaceStatFromProvider();
+ return;
+ }
var lines = File.ReadAllLines("/proc/net/dev");
var now = DateTime.Now;
@@ -143,6 +157,61 @@ public class StatCollector
}
}
+ /// <summary>通过 INetworkStatProvider 采集网口流量(Windows/macOS 等非 Linux 平台)</summary>
+ private void CollectInterfaceStatFromProvider()
+ {
+ if (_statProvider == null) return;
+
+ try
+ {
+ var stats = _statProvider.GetInterfaceStatsAsync().GetAwaiter().GetResult();
+ var now = DateTime.Now;
+ var statTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0);
+
+ foreach (var s in stats)
+ {
+ // 计算差值
+ Int64 rxDelta = s.RxBytes;
+ Int64 txDelta = s.TxBytes;
+ if (_lastNetStats.TryGetValue(s.Name, out var last) && last.time > DateTime.MinValue)
+ {
+ rxDelta = s.RxBytes - last.rx;
+ txDelta = s.TxBytes - last.tx;
+ if (rxDelta < 0) rxDelta = 0;
+ if (txDelta < 0) txDelta = 0;
+ }
+ _lastNetStats[s.Name] = (s.RxBytes, s.TxBytes, now);
+
+ // 跳过无流量的接口
+ if (rxDelta == 0 && txDelta == 0) continue;
+
+ // Windows 网口名可能过长(如 "Realtek PCIe GbE Family Controller"),截断到 20 字符
+ var ifName = s.Name;
+ if (ifName != null && ifName.Length > 20)
+ ifName = ifName.Substring(0, 20);
+
+ var stat = new InterfaceStat
+ {
+ InterfaceName = ifName,
+ RxBytes = rxDelta,
+ RxPackets = s.RxPackets > 0 ? s.RxPackets : 0,
+ TxPackets = s.TxPackets > 0 ? s.TxPackets : 0,
+ RxErrors = s.RxErrors,
+ TxErrors = s.TxErrors,
+ RxDropped = s.RxDropped,
+ TxDropped = s.TxDropped,
+ StatTime = statTime,
+ };
+
+ stat.Insert();
+ }
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"[StatCollector] 通过 Provider 采集网口统计失败: {ex.Message}");
+ }
+ }
+
#endregion
#region 设备流量采集
diff --git a/Rainbow.Data/Services/WanManager.cs b/Rainbow.Data/Services/WanManager.cs
index 43d5030..95775f2 100644
--- a/Rainbow.Data/Services/WanManager.cs
+++ b/Rainbow.Data/Services/WanManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NewLife;
@@ -29,13 +30,28 @@ public class WanManager
if (Shell == null) return ShellResult.Fail("Shell 未配置");
var wans = WanInterface.FindAll(WanInterface._.Enable == true);
var table = 100;
+ var isWindows = OperatingSystem.IsWindows();
+
foreach (var wan in wans)
{
if (wan.Gateway.IsNullOrEmpty() || wan.Name.IsNullOrEmpty()) continue;
wan.RouteTable = table;
- var result = await Shell.ExecuteAsync("ip", $"route add default via {wan.Gateway} dev {wan.Name} table {table}");
- if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
- if (!wan.IP.IsNullOrEmpty()) await Shell.ExecuteAsync("ip", $"rule add from {wan.IP} table {table}");
+
+ if (isWindows)
+ {
+ // Windows: route.exe add MASK 网关
+ var metric = 256 - (table - 100); // table 越小 metric 越小(越优先)
+ var result = await Shell.ExecuteAsync("route", $"add 0.0.0.0 mask 0.0.0.0 {wan.Gateway} metric {metric} if {wan.Name}");
+ if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
+ }
+ else
+ {
+ // Linux: ip route add default via gateway dev interface table tableId
+ var result = await Shell.ExecuteAsync("ip", $"route add default via {wan.Gateway} dev {wan.Name} table {table}");
+ if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
+ if (!wan.IP.IsNullOrEmpty()) await Shell.ExecuteAsync("ip", $"rule add from {wan.IP} table {table}");
+ }
+
wan.Update(); Log.Info("WAN[{0}] 路由表 {1} 已配置", wan.Name, table); table++;
}
return new ShellResult { Stdout = $"已为 {wans.Count} 个 WAN 口配置路由表" };
@@ -66,16 +82,21 @@ public class WanManager
if (Shell == null) return;
var expWan = new WhereExpression(); expWan &= WanInterface._.Enable == true; expWan &= WanInterface._.IsDefault == true;
var wans = WanInterface.FindAll(expWan);
+ var isWindows = OperatingSystem.IsWindows();
foreach (var wan in wans)
{
if (wan.Gateway.IsNullOrEmpty()) continue;
- var result = await Shell.ExecuteAsync("ping", $"-c 1 -W 3 {wan.Gateway}");
+ var pingArgs = isWindows ? $"-n 1 -w 3000 {wan.Gateway}" : $"-c 1 -W 3 {wan.Gateway}";
+ var result = await Shell.ExecuteAsync("ping", pingArgs);
if (result.Success) continue;
Log.Warn("WAN[{0}] 网关不可达,切换...", wan.Name);
var expFb = new WhereExpression(); expFb &= WanInterface._.Enable == true; expFb &= WanInterface._.Id != wan.Id;
var fallback = WanInterface.FindAll(expFb).FirstOrDefault(f => !f.IsDefault);
if (fallback == null) continue;
- await Shell.ExecuteAsync("ip", $"route replace default via {fallback.Gateway} dev {fallback.Name}");
+ if (isWindows)
+ await Shell.ExecuteAsync("route", $"add 0.0.0.0 mask 0.0.0.0 {fallback.Gateway} metric 10");
+ else
+ await Shell.ExecuteAsync("ip", $"route replace default via {fallback.Gateway} dev {fallback.Name}");
wan.IsDefault = false; wan.Update(); fallback.IsDefault = true; fallback.Update();
Log.Info("WAN 故障切换:{0} → {1}", wan.Name, fallback.Name);
}
diff --git a/Rainbow.Web/Program.cs b/Rainbow.Web/Program.cs
index 9322c6d..5041b01 100644
--- a/Rainbow.Web/Program.cs
+++ b/Rainbow.Web/Program.cs
@@ -30,34 +30,6 @@ services.AddSingleton<ConfigBackup>();
// 跨平台适配器
var osAdapter = AdapterFactory.Create();
services.AddSingleton<IOSAdapter>(osAdapter);
-services.AddSingleton<MonitorCollector>();
-services.AddSingleton<WanManager>();
-services.AddSingleton<PppManager>();
-services.AddSingleton<FirewallManager>();
-services.AddSingleton<StatCollector>();
-services.AddSingleton<StatAggregator>();
-services.AddSingleton<DeviceHistoryTracker>();
-services.AddSingleton<MemberQuotaService>();
-services.AddSingleton<UpgradeService>();
-
-// DHCP/DNS 适配器(按平台注册)
-services.AddSingleton<IDhcpDnsAdapter>(sp =>
-{
- if (OperatingSystem.IsLinux())
- {
- var shell = sp.GetRequiredService<ShellExecutor>();
- return new DnsmasqAdapter(shell);
- }
- if (OperatingSystem.IsWindows())
- return new WindowsDhcpDnsAdapter();
- return new DnsmasqAdapter(sp.GetRequiredService<ShellExecutor>());
-});
-
-// Dnsmasq 配置管理器(单例)
-services.AddSingleton<DnsmasqManager>();
-
-// DNS 日志解析器(单例,定时更新黑名单命中统计)
-services.AddSingleton<DnsLogParser>();
// 网络管理接口(按平台注册具体实现)
services.AddSingleton<INetworkInterfaceManager>(sp =>
@@ -72,6 +44,8 @@ services.AddSingleton<INetworkRouteManager>(sp =>
return new WindowsRouteManager();
return new StubNetworkRouteManager();
});
+
+// 网口流量统计提供者(按平台注册)
services.AddSingleton<INetworkStatProvider>(sp =>
{
if (OperatingSystem.IsWindows())
@@ -79,6 +53,42 @@ services.AddSingleton<INetworkStatProvider>(sp =>
return new StubNetworkStatProvider();
});
+// 防火墙适配器(按平台注册)
+services.AddSingleton<IFirewallAdapter>(sp =>
+{
+ if (OperatingSystem.IsWindows())
+ return new WindowsFirewallAdapter();
+ // Linux 上根据发行版选择 iptables/nftables
+ var shell = sp.GetRequiredService<ShellExecutor>();
+ return new IptablesAdapter(shell);
+});
+
+// DHCP/DNS 适配器(按平台注册)
+services.AddSingleton<IDhcpDnsAdapter>(sp =>
+{
+ if (OperatingSystem.IsLinux())
+ {
+ var shell = sp.GetRequiredService<ShellExecutor>();
+ return new DnsmasqAdapter(shell);
+ }
+ if (OperatingSystem.IsWindows())
+ return new WindowsDhcpDnsAdapter();
+ return new DnsmasqAdapter(sp.GetRequiredService<ShellExecutor>());
+});
+
+// 服务注册(含平台感知的注入)
+services.AddSingleton<MonitorCollector>();
+services.AddSingleton<WanManager>();
+services.AddSingleton<PppManager>();
+services.AddSingleton<FirewallManager>();
+services.AddSingleton<DnsmasqManager>();
+services.AddSingleton<DnsLogParser>();
+services.AddSingleton<StatCollector>();
+services.AddSingleton<StatAggregator>();
+services.AddSingleton<DeviceHistoryTracker>();
+services.AddSingleton<MemberQuotaService>();
+services.AddSingleton<UpgradeService>();
+
// 拦截提示页服务(端口 9080)
services.AddHostedService<BlockPageService>();