Add restricted Link policy controls
This commit is contained in:
parent
b92bb75b08
commit
231ba69754
5 changed files with 114 additions and 14 deletions
|
|
@ -48,8 +48,8 @@
|
||||||
|
|
||||||
- [x] направленные пары source → destination;
|
- [x] направленные пары source → destination;
|
||||||
- [x] ручное добавление и удаление nftables ACL из Windows-клиента;
|
- [x] ручное добавление и удаление nftables ACL из Windows-клиента;
|
||||||
- [ ] политики по CIDR, TCP/UDP и порту;
|
- [x] политики по целевому `/32`, TCP/UDP и порту;
|
||||||
- [ ] TTL и автоматическое истечение;
|
- [x] TTL и автоматическое истечение;
|
||||||
- [ ] состояния Connecting, Active, Disconnecting, Partial, Disabled и Failed;
|
- [ ] состояния Connecting, Active, Disconnecting, Partial, Disabled и Failed;
|
||||||
- [ ] версия политики и подтверждение применения;
|
- [ ] версия политики и подтверждение применения;
|
||||||
- [ ] обязательное отключение после reconnect;
|
- [ ] обязательное отключение после reconnect;
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,36 @@
|
||||||
Header="Целевой сервер"
|
Header="Целевой сервер"
|
||||||
ItemsSource="{x:Bind MeshNodes}" />
|
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 ColumnSpacing="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|
@ -265,9 +295,10 @@
|
||||||
|
|
||||||
<TextBlock FontWeight="SemiBold" Text="Разрешённые направления" />
|
<TextBlock FontWeight="SemiBold" Text="Разрешённые направления" />
|
||||||
<ListView
|
<ListView
|
||||||
|
x:Name="MeshLinksList"
|
||||||
MaxHeight="220"
|
MaxHeight="220"
|
||||||
ItemsSource="{x:Bind MeshLinks}"
|
ItemsSource="{x:Bind MeshLinks}"
|
||||||
SelectionMode="None">
|
SelectionMode="Single">
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="local:MeshLinkViewModel">
|
<DataTemplate x:DataType="local:MeshLinkViewModel">
|
||||||
<TextBlock Margin="4,8" Text="{x:Bind Label}" />
|
<TextBlock Margin="4,8" Text="{x:Bind Label}" />
|
||||||
|
|
|
||||||
|
|
@ -306,9 +306,17 @@ public sealed partial class MainPage : Page
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var fields = line[5..].Split('|');
|
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);
|
ShowInfo("Mesh Hub не выбран", "Сначала добавьте главный сервер с отметкой Mesh Hub.", InfoBarSeverity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (SourceNodeBox.SelectedItem is not MeshNodeViewModel source
|
MeshNodeViewModel? source = null;
|
||||||
|| TargetNodeBox.SelectedItem is not MeshNodeViewModel target)
|
MeshNodeViewModel? target = null;
|
||||||
|
string protocol;
|
||||||
|
int port;
|
||||||
|
int ttlMinutes;
|
||||||
|
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
if (SourceNodeBox.SelectedItem is not MeshNodeViewModel selectedSource
|
||||||
|
|| TargetNodeBox.SelectedItem is not MeshNodeViewModel selectedTarget)
|
||||||
{
|
{
|
||||||
ShowInfo("Выберите серверы", "Укажите источник и сервер назначения.", InfoBarSeverity.Warning);
|
ShowInfo("Выберите серверы", "Укажите источник и сервер назначения.", InfoBarSeverity.Warning);
|
||||||
return;
|
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)
|
if (source.Name == target.Name)
|
||||||
{
|
{
|
||||||
ShowInfo("Некорректная связь", "Источник и назначение должны отличаться.", InfoBarSeverity.Warning);
|
ShowInfo("Некорректная связь", "Источник и назначение должны отличаться.", InfoBarSeverity.Warning);
|
||||||
|
|
@ -358,15 +407,18 @@ public sealed partial class MainPage : Page
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var action = enable ? "connect" : "disconnect";
|
var action = enable ? "connect" : "disconnect";
|
||||||
|
var policyArguments = enable
|
||||||
|
? $"{protocol} {port} {ttlMinutes}"
|
||||||
|
: $"{protocol} {port}";
|
||||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||||
await _ssh.RunRestrictedCommandAsync(
|
await _ssh.RunRestrictedCommandAsync(
|
||||||
hub.Profile,
|
hub.Profile,
|
||||||
$"mesh {action} {source.Name} {target.Name}",
|
$"mesh {action} {source.Name} {target.Name} {policyArguments}",
|
||||||
timeout.Token);
|
timeout.Token);
|
||||||
await RefreshMeshAsync(showSuccess: false);
|
await RefreshMeshAsync(showSuccess: false);
|
||||||
ShowInfo(
|
ShowInfo(
|
||||||
enable ? "Связь включена" : "Связь отключена",
|
enable ? "Связь включена" : "Связь отключена",
|
||||||
$"{source.Name} → {target.Name}",
|
$"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port}",
|
||||||
enable ? InfoBarSeverity.Success : InfoBarSeverity.Warning);
|
enable ? InfoBarSeverity.Success : InfoBarSeverity.Warning);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,30 @@ public sealed class MeshNodeViewModel
|
||||||
|
|
||||||
public sealed class MeshLinkViewModel
|
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;
|
Source = source;
|
||||||
Target = target;
|
Target = target;
|
||||||
|
Cidr = cidr;
|
||||||
|
Protocol = protocol;
|
||||||
|
Port = port;
|
||||||
|
ExpiresUnix = expiresUnix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
public string Target { 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)]
|
[GeneratedRegex("^[a-z_][a-z0-9_-]{0,31}$", RegexOptions.CultureInvariant)]
|
||||||
private static partial Regex SafeUserRegex();
|
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();
|
private static partial Regex SafeRestrictedCommandRegex();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue