Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions scripts/sync-perf-tweaks.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Syncs performance tweak scripts from the unifi-perf-tweaks source repo
# into the NetworkOptimizer embedded resources directory.
#
# Run before building to pick up the latest scripts:
# pwsh scripts/sync-perf-tweaks.ps1
#
# Source repo: https://github.com/tvancott42/unifi-perf-tweaks (private)

param(
[string]$SourceRepo = "$env:USERPROFILE\OneDrive\PersonalProjects\OpenSource\unifi-perf-tweaks"
)

$DestDir = Join-Path $PSScriptRoot "..\src\NetworkOptimizer.Web\Resources\PerfTweaks"

if (-not (Test-Path $SourceRepo)) {
Write-Error "Source repo not found at: $SourceRepo"
exit 1
}

if (-not (Test-Path $DestDir)) {
New-Item -ItemType Directory -Path $DestDir -Force | Out-Null
}

$scripts = @(
"scripts/06-mongodb-ssd-offload.sh",
"scripts/07-mongodb-ssd-backup.sh",
"scripts/10-journald-volatile.sh",
"scripts/15-fan-control-tuning.sh",
"scripts/20-sfp-sgmiiplus.sh"
)

$binaries = @(
"modules/force-uniphy1-sgmiiplus/force_uniphy1_sgmiiplus.ko"
)

foreach ($file in $scripts + $binaries) {
$src = Join-Path $SourceRepo $file
$dest = Join-Path $DestDir (Split-Path $file -Leaf)
if (Test-Path $src) {
Copy-Item $src $dest -Force
Write-Host " Copied: $(Split-Path $file -Leaf)"
} else {
Write-Warning " Missing: $file"
}
}

Write-Host "Sync complete."
42 changes: 42 additions & 0 deletions src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,29 @@ private DeviceDetectionResult DetectDeviceTypeCore(
};
}

// Priority -0.5: Known UNAS/Drive devices (from V2 API drive_devices array).
// These share Ubiquiti OUI prefixes with cameras but are NAS storage devices.
if (_protectCameras != null && !string.IsNullOrEmpty(client?.Mac) &&
_protectCameras.IsDriveDevice(client.Mac))
{
_logger?.LogDebug("[Detection] '{DisplayName}': Known UNAS/Drive device (confirmed by controller API)",
displayName);
return new DeviceDetectionResult
{
Category = ClientDeviceCategory.NAS,
Source = DetectionSource.UniFiFingerprint,
ConfidenceScore = 100,
VendorName = "Ubiquiti",
ProductName = client.Name,
RecommendedNetwork = NetworkPurpose.Corporate,
Metadata = new Dictionary<string, object>
{
["detection_method"] = "unifi_network_api",
["mac"] = client.Mac
}
};
}

// Priority 0: Check for obvious name keywords that should OVERRIDE fingerprint
// This handles cases where vendor fingerprint is wrong (e.g., Cync plugs detected as cameras)
var obviousNameResult = CheckObviousNameOverride(client?.Name, client?.Hostname, client?.Oui);
Expand Down Expand Up @@ -1358,6 +1381,25 @@ public DeviceDetectionResult DetectFromMac(string macAddress)
if (string.IsNullOrEmpty(macAddress))
return DeviceDetectionResult.Unknown;

// Check for known UNAS/Drive devices before any other detection
if (_protectCameras != null && _protectCameras.IsDriveDevice(macAddress))
{
_logger?.LogDebug("[Detection] MAC {Mac}: Known UNAS/Drive device (confirmed by controller API)", macAddress);
return new DeviceDetectionResult
{
Category = ClientDeviceCategory.NAS,
Source = DetectionSource.UniFiFingerprint,
ConfidenceScore = 100,
VendorName = "Ubiquiti",
RecommendedNetwork = NetworkPurpose.Corporate,
Metadata = new Dictionary<string, object>
{
["detection_method"] = "unifi_network_api",
["mac"] = macAddress
}
};
}

// First, check if we have this MAC in client history (for fingerprint data)
if (_clientHistoryByMac != null &&
_clientHistoryByMac.TryGetValue(macAddress.ToLowerInvariant(), out var historyClient))
Expand Down
33 changes: 28 additions & 5 deletions src/NetworkOptimizer.Core/Helpers/CloudflareIpRanges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ public static class CloudflareIpRanges
/// </summary>
public static readonly string[] AllRanges = [.. IPv4Ranges, .. IPv6Ranges];

private const int MaxNonCloudflareHosts = 10;

/// <summary>
/// Check if a list of IP addresses/CIDRs represents a Cloudflare-only restriction.
/// Returns true if every entry in the list is a known Cloudflare range.
/// Check if a list of IP addresses/CIDRs represents a Cloudflare-restricted configuration.
/// Returns true if the list is primarily Cloudflare ranges, tolerating a small number of
/// individual host IPs (e.g., management/home IPs) that people commonly add alongside CF ranges.
/// Non-Cloudflare entries must be single hosts (no CIDR or /32 or /128) - large non-CF subnets
/// indicate a different restriction strategy, not a Cloudflare list with extras.
/// </summary>
/// <param name="addresses">List of IPs or CIDRs from a firewall group or source restriction</param>
/// <returns>True if all addresses match known Cloudflare ranges</returns>
/// <returns>True if the list is a Cloudflare IP restriction (possibly with a few management IPs)</returns>
public static bool IsCloudflareOnly(IEnumerable<string>? addresses)
{
if (addresses == null)
Expand All @@ -64,13 +69,31 @@ public static bool IsCloudflareOnly(IEnumerable<string>? addresses)
if (list.Count == 0)
return false;

int cloudflareCount = 0;
int nonCloudflareHostCount = 0;

foreach (var address in list)
{
if (!IsCloudflareAddress(address))
if (IsCloudflareAddress(address))
{
cloudflareCount++;
continue;
}

var trimmed = address.Trim();
bool isSingleHost = !trimmed.Contains('/')
|| trimmed.EndsWith("/32")
|| trimmed.EndsWith("/128");

if (!isSingleHost)
return false;

nonCloudflareHostCount++;
if (nonCloudflareHostCount > MaxNonCloudflareHosts)
return false;
}

return true;
return cloudflareCount > 0;
}

/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions src/NetworkOptimizer.Core/Models/ProtectCamera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ public bool IsNvr(string? mac)
/// </summary>
public IEnumerable<ProtectCamera> GetAll() => _cameras.Values;

/// <summary>
/// MACs of known UNAS/Drive devices (from drive_devices in V2 API).
/// These share Ubiquiti OUI prefixes with cameras but are NOT cameras.
/// </summary>
private readonly HashSet<string> _driveDeviceMacs = new(StringComparer.OrdinalIgnoreCase);

public void AddDriveDevice(string mac)
{
_driveDeviceMacs.Add(mac.ToLowerInvariant());
}

public bool IsDriveDevice(string? mac)
{
return !string.IsNullOrEmpty(mac) && _driveDeviceMacs.Contains(mac);
}

public int DriveDeviceCount => _driveDeviceMacs.Count;

/// <summary>
/// Create an empty collection
/// </summary>
Expand Down
125 changes: 122 additions & 3 deletions src/NetworkOptimizer.Diagnostics/Analyzers/PerformanceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ public List<PerformanceIssue> Analyze(
JsonDocument? qosRulesData,
JsonDocument? wanEnrichedData = null,
bool runPerformanceChecks = true,
bool runCellularChecks = true)
bool runCellularChecks = true,
List<UniFiPortProfile>? portProfiles = null)
{
var issues = new List<PerformanceIssue>();

if (runPerformanceChecks)
{
issues.AddRange(CheckHardwareAcceleration(devices, settingsData));
issues.AddRange(CheckJumboFrames(devices, settingsData));
issues.AddRange(CheckFlowControl(devices, networks, clients, settingsData));
issues.AddRange(CheckFlowControl(devices, networks, clients, settingsData, portProfiles));
}

if (runCellularChecks)
Expand Down Expand Up @@ -260,7 +261,8 @@ internal List<PerformanceIssue> CheckFlowControl(
List<UniFiDeviceResponse> devices,
List<UniFiNetworkConfig> networks,
List<UniFiClientResponse> clients,
JsonDocument? settingsData)
JsonDocument? settingsData,
List<UniFiPortProfile>? portProfiles = null)
{
var issues = new List<PerformanceIssue>();
var settings = GlobalSwitchSettings.FromSettingsJson(settingsData);
Expand Down Expand Up @@ -312,6 +314,9 @@ internal List<PerformanceIssue> CheckFlowControl(
});
}
}

// Check port profiles and per-port overrides
issues.AddRange(CheckFlowControlPortProfiles(devices, portProfiles, settings));
}
else
{
Expand Down Expand Up @@ -533,6 +538,120 @@ internal List<PerformanceIssue> CheckCellularQos(
return issues;
}

/// <summary>
/// When global Flow Control is ON, check for port profiles and individual switch ports
/// that have Flow Control explicitly disabled - creating inconsistency with the global setting.
/// </summary>
[VendorSpecific("UniFi", "Reads FlowControlEnabled from port profiles and flow_control_enabled from switch port_table")]
internal List<PerformanceIssue> CheckFlowControlPortProfiles(
List<UniFiDeviceResponse> devices,
List<UniFiPortProfile>? portProfiles,
GlobalSwitchSettings? settings)
{
var issues = new List<PerformanceIssue>();

// Build profile lookup if available
var profilesById = portProfiles?.ToDictionary(p => p.Id, StringComparer.OrdinalIgnoreCase)
?? new Dictionary<string, UniFiPortProfile>(StringComparer.OrdinalIgnoreCase);

// Find profiles with FC explicitly disabled
if (portProfiles != null)
{
var fcOffProfiles = portProfiles
.Where(p => p.FlowControlEnabled == false && p.Forward != "disabled")
.ToList();
if (fcOffProfiles.Count > 0)
{
_logger?.LogDebug("Found {Count} port profiles with Flow Control disabled", fcOffProfiles.Count);

foreach (var profile in fcOffProfiles)
{
var profileName = HtmlEncode(profile.Name);
issues.Add(new PerformanceIssue
{
Title = $"Flow Control Disabled in Profile \"{profile.Name}\"",
Description = $"Flow Control is enabled globally, but the Ethernet Port Profile " +
$"\"{profile.Name}\" has Flow Control disabled. Any port assigned to this profile " +
"will not use Flow Control, overriding the global setting.",
Recommendation = $"If this isn't intentional, enable Flow Control in the " +
$"\"{profileName}\" port profile or remove the override so it inherits the global setting.",
Severity = PerformanceSeverity.Info,
Category = PerformanceCategory.Performance
});
}
}
}

// Find ports on each switch with FC off (via port_table field or profile override)
foreach (var device in devices)
{
if (device.PortTable == null || device.DeviceType == DeviceType.Gateway)
continue;

// Only check devices where FC would otherwise be on
bool deviceFcOn = settings?.GetEffectiveFlowControl(device) ?? false;
if (!deviceFcOn)
continue;

var affectedPorts = new List<string>();
foreach (var port in device.PortTable)
{
if (port.Forward == "disabled")
continue;

bool portFcOff;
string? profileName = null;

if (!string.IsNullOrEmpty(port.PortConfId) &&
profilesById.TryGetValue(port.PortConfId, out var profile))
{
if (profile.Forward == "disabled")
continue;

// Profile takes precedence when it explicitly sets FC;
// fall back to port's own field when profile doesn't set it
portFcOff = profile.FlowControlEnabled.HasValue
? profile.FlowControlEnabled == false
: port.FlowControlEnabled == false;
profileName = profile.Name;
}
else
{
portFcOff = port.FlowControlEnabled == false;
}

if (portFcOff)
{
affectedPorts.Add(profileName != null
? $"{port.Name} (\"{profileName}\")"
: port.Name);
}
}

if (affectedPorts.Count > 0)
{
var deviceName = HtmlEncode(device.Name);
var portList = string.Join(", ", affectedPorts);
issues.Add(new PerformanceIssue
{
Title = $"Flow Control Overridden on {device.Name}",
Description = $"Flow Control is enabled globally, but {affectedPorts.Count} " +
$"port(s) on {device.Name} have it disabled: {portList}.",
Recommendation = $"If this isn't intentional, enable Flow Control on these ports " +
$"in UniFi Devices > {deviceName} > Port Manager.",
Severity = PerformanceSeverity.Info,
Category = PerformanceCategory.Performance,
DeviceName = device.Name
});

_logger?.LogDebug("Device {Name}: {Count} ports with FC disabled: {Ports}",
device.Name, affectedPorts.Count, portList);
}
}

return issues;
}

#region Helper Methods

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/NetworkOptimizer.Diagnostics/DiagnosticsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ public DiagnosticsResult RunDiagnostics(
result.PerformanceIssues = _performanceAnalyzer.Analyze(
deviceList, networkList, clientList, settingsData, qosRulesData, wanEnrichedData,
runPerformanceChecks: options.RunPerformanceAnalyzer,
runCellularChecks: options.RunCellularDataSavings);
runCellularChecks: options.RunCellularDataSavings,
portProfiles: profileList);
result.CellularWanDetected = _performanceAnalyzer.CellularWanDetected;
_logger?.LogDebug("Performance Analyzer found {Count} issues", result.PerformanceIssues.Count);
}
Expand Down
Loading
Loading