diff --git a/docs/roadmap.md b/docs/roadmap.md
index 07be447..6abbfb8 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -48,8 +48,8 @@
- [x] направленные пары source → destination;
- [x] ручное добавление и удаление nftables ACL из Windows-клиента;
-- [ ] политики по CIDR, TCP/UDP и порту;
-- [ ] TTL и автоматическое истечение;
+- [x] политики по целевому `/32`, TCP/UDP и порту;
+- [x] TTL и автоматическое истечение;
- [ ] состояния Connecting, Active, Disconnecting, Partial, Disabled и Failed;
- [ ] версия политики и подтверждение применения;
- [ ] обязательное отключение после reconnect;
diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml b/src/ServerMonitorManager.Desktop/MainPage.xaml
index dcff39c..d37793a 100644
--- a/src/ServerMonitorManager.Desktop/MainPage.xaml
+++ b/src/ServerMonitorManager.Desktop/MainPage.xaml
@@ -244,6 +244,36 @@
Header="Целевой сервер"
ItemsSource="{x:Bind MeshNodes}" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -265,9 +295,10 @@
+ SelectionMode="Single">
diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
index 192dc6b..05b53a2 100644
--- a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
+++ b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
@@ -306,9 +306,17 @@ public sealed partial class MainPage : Page
continue;
}
var fields = line[5..].Split('|');
- if (fields.Length >= 2)
+ if (fields.Length >= 7
+ && int.TryParse(fields[4], CultureInfo.InvariantCulture, out var port)
+ && long.TryParse(fields[5], CultureInfo.InvariantCulture, out var expiresUnix))
{
- MeshLinks.Add(new MeshLinkViewModel(fields[0], fields[1]));
+ MeshLinks.Add(new MeshLinkViewModel(
+ fields[0],
+ fields[1],
+ fields[2],
+ fields[3],
+ port,
+ expiresUnix));
}
}
@@ -343,12 +351,53 @@ public sealed partial class MainPage : Page
ShowInfo("Mesh Hub не выбран", "Сначала добавьте главный сервер с отметкой Mesh Hub.", InfoBarSeverity.Warning);
return;
}
- if (SourceNodeBox.SelectedItem is not MeshNodeViewModel source
- || TargetNodeBox.SelectedItem is not MeshNodeViewModel target)
+ MeshNodeViewModel? source = null;
+ MeshNodeViewModel? target = null;
+ string protocol;
+ int port;
+ int ttlMinutes;
+
+ if (enable)
{
- ShowInfo("Выберите серверы", "Укажите источник и сервер назначения.", InfoBarSeverity.Warning);
- return;
+ if (SourceNodeBox.SelectedItem is not MeshNodeViewModel selectedSource
+ || TargetNodeBox.SelectedItem is not MeshNodeViewModel selectedTarget)
+ {
+ ShowInfo("Выберите серверы", "Укажите источник и сервер назначения.", InfoBarSeverity.Warning);
+ return;
+ }
+ source = selectedSource;
+ target = selectedTarget;
+ protocol = (LinkProtocolBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "tcp";
+ if (double.IsNaN(LinkPortBox.Value)
+ || double.IsNaN(LinkTtlBox.Value)
+ || LinkPortBox.Value is < 1 or > 65535
+ || LinkTtlBox.Value is < 0 or > 525600)
+ {
+ ShowInfo("Некорректная политика", "Проверьте порт и TTL.", InfoBarSeverity.Warning);
+ return;
+ }
+ port = checked((int)LinkPortBox.Value);
+ ttlMinutes = checked((int)LinkTtlBox.Value);
}
+ else
+ {
+ if (MeshLinksList.SelectedItem is not MeshLinkViewModel selectedLink)
+ {
+ ShowInfo("Выберите связь", "Для отключения выберите правило в списке.", InfoBarSeverity.Warning);
+ return;
+ }
+ source = MeshNodes.FirstOrDefault(node => node.Name == selectedLink.Source);
+ target = MeshNodes.FirstOrDefault(node => node.Name == selectedLink.Target);
+ if (source is null || target is null)
+ {
+ ShowInfo("Узел не найден", "Обновите список Mesh и повторите попытку.", InfoBarSeverity.Warning);
+ return;
+ }
+ protocol = selectedLink.Protocol;
+ port = selectedLink.Port;
+ ttlMinutes = 0;
+ }
+
if (source.Name == target.Name)
{
ShowInfo("Некорректная связь", "Источник и назначение должны отличаться.", InfoBarSeverity.Warning);
@@ -358,15 +407,18 @@ public sealed partial class MainPage : Page
try
{
var action = enable ? "connect" : "disconnect";
+ var policyArguments = enable
+ ? $"{protocol} {port} {ttlMinutes}"
+ : $"{protocol} {port}";
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await _ssh.RunRestrictedCommandAsync(
hub.Profile,
- $"mesh {action} {source.Name} {target.Name}",
+ $"mesh {action} {source.Name} {target.Name} {policyArguments}",
timeout.Token);
await RefreshMeshAsync(showSuccess: false);
ShowInfo(
enable ? "Связь включена" : "Связь отключена",
- $"{source.Name} → {target.Name}",
+ $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port}",
enable ? InfoBarSeverity.Success : InfoBarSeverity.Warning);
}
catch (Exception exception)
diff --git a/src/ServerMonitorManager.Desktop/MeshModels.cs b/src/ServerMonitorManager.Desktop/MeshModels.cs
index 14148bc..60f6c2f 100644
--- a/src/ServerMonitorManager.Desktop/MeshModels.cs
+++ b/src/ServerMonitorManager.Desktop/MeshModels.cs
@@ -19,13 +19,30 @@ public sealed class MeshNodeViewModel
public sealed class MeshLinkViewModel
{
- public MeshLinkViewModel(string source, string target)
+ public MeshLinkViewModel(
+ string source,
+ string target,
+ string cidr,
+ string protocol,
+ int port,
+ long expiresUnix)
{
Source = source;
Target = target;
+ Cidr = cidr;
+ Protocol = protocol;
+ Port = port;
+ ExpiresUnix = expiresUnix;
}
public string Source { get; set; }
public string Target { get; set; }
- public string Label => $"{Source} → {Target}";
+ public string Cidr { get; set; }
+ public string Protocol { get; set; }
+ public int Port { get; set; }
+ public long ExpiresUnix { get; set; }
+ public string ExpirationText => ExpiresUnix == 0
+ ? "вручную"
+ : $"до {DateTimeOffset.FromUnixTimeSeconds(ExpiresUnix).ToLocalTime():dd.MM HH:mm}";
+ public string Label => $"{Source} → {Target} · {Protocol.ToUpperInvariant()}/{Port} · {Cidr} · {ExpirationText}";
}
diff --git a/src/ServerMonitorManager.Desktop/SshMonitorService.cs b/src/ServerMonitorManager.Desktop/SshMonitorService.cs
index 46ad15f..0e5eac9 100644
--- a/src/ServerMonitorManager.Desktop/SshMonitorService.cs
+++ b/src/ServerMonitorManager.Desktop/SshMonitorService.cs
@@ -201,6 +201,6 @@ public sealed partial class SshMonitorService
[GeneratedRegex("^[a-z_][a-z0-9_-]{0,31}$", RegexOptions.CultureInvariant)]
private static partial Regex SafeUserRegex();
- [GeneratedRegex("^(metrics|mesh (nodes|links|status|connect [a-z0-9][a-z0-9-]{0,31} [a-z0-9][a-z0-9-]{0,31}|disconnect [a-z0-9][a-z0-9-]{0,31} [a-z0-9][a-z0-9-]{0,31}))$", RegexOptions.CultureInvariant)]
+ [GeneratedRegex("^(metrics|mesh (nodes|links|status|connect [a-z0-9][a-z0-9-]{0,31} [a-z0-9][a-z0-9-]{0,31} (tcp|udp) [0-9]{1,5} [0-9]{1,6}|disconnect [a-z0-9][a-z0-9-]{0,31} [a-z0-9][a-z0-9-]{0,31} (tcp|udp) [0-9]{1,5}))$", RegexOptions.CultureInvariant)]
private static partial Regex SafeRestrictedCommandRegex();
}