Add Windows SSH monitoring MVP #1
5 changed files with 114 additions and 14 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -244,6 +244,36 @@
|
|||
Header="Целевой сервер"
|
||||
ItemsSource="{x:Bind MeshNodes}" />
|
||||
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ComboBox
|
||||
x:Name="LinkProtocolBox"
|
||||
Header="Протокол"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem Content="TCP" Tag="tcp" />
|
||||
<ComboBoxItem Content="UDP" Tag="udp" />
|
||||
</ComboBox>
|
||||
<NumberBox
|
||||
x:Name="LinkPortBox"
|
||||
Grid.Column="1"
|
||||
Header="Порт"
|
||||
Maximum="65535"
|
||||
Minimum="1"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="22" />
|
||||
</Grid>
|
||||
|
||||
<NumberBox
|
||||
x:Name="LinkTtlBox"
|
||||
Header="TTL, минут (0 — до ручного отключения)"
|
||||
Maximum="525600"
|
||||
Minimum="0"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="120" />
|
||||
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
|
|
@ -265,9 +295,10 @@
|
|||
|
||||
<TextBlock FontWeight="SemiBold" Text="Разрешённые направления" />
|
||||
<ListView
|
||||
x:Name="MeshLinksList"
|
||||
MaxHeight="220"
|
||||
ItemsSource="{x:Bind MeshLinks}"
|
||||
SelectionMode="None">
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:MeshLinkViewModel">
|
||||
<TextBlock Margin="4,8" Text="{x:Bind Label}" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue